Window API -- InitializeCriticalSectionAndSpinCount()

实际上对 CRITICAL_SECTION 的操作非常轻量,为什么还要加上旋转锁的动作呢?其实这个函数在单cpu的电脑上是不起作用的,只有当电脑上存在不止一个cpu,或者一个cpu但多核的时候,才管用。
 
如果临界区用来保护的操作耗时非常短暂,比如就是保护一个reference counter,或者某一个flag,那么几个时钟周期以后就会离开临界区。可是当这个thread还没有离开临界区之前,另外一个thread试图进入此临界区——这种情况只会发生在多核或者smp的系统上——发现无法进入,于是这个thread会进入睡眠,然后会发生一次上下文切换。我们知道 context switch是一个比较耗时的操作,据说需要数千个时钟周期,那么其实我们只要再等多几个时钟周期就能够进入临界区,现在却多了数千个时钟周期的开销,真是是可忍孰不可忍。
 
所以就引入了InitializeCriticalSectionAndSpinCount函数,它的第一个参数是指向cs的指针,第二个参数是旋转的次数。我的理解就是一个循环次数,比如说N,那么就是说此时EnterCriticalSection()函数会内部循环判断此临界区是否可以进入,直到可以进入或者N次满。我们增加的开销是最多N次循环,我们可能获得的红利是数千个时钟周期。对于临界区内很短的操作来讲,这样做的好处是大大的。
 
MSDN上说,他们对于堆管理器使用了N=4000的旋转锁,然后“This gives great performance and scalability in almost all worst-case scenarios.”
 

The process is responsible for allocating the memory used by a critical section object, which it can do by declaring a variable of typeCRITICAL_SECTION. Before using a critical section, some thread of the process must initialize the object. You can subsequently modify the spin count by calling theSetCriticalSectionSpinCount function.

After a critical section object is initialized, the threads of the process can specify the object in theEnterCriticalSection,TryEnterCriticalSection, or LeaveCriticalSection function to provide mutually exclusive access to a shared resource. For similar synchronization between the threads of different processes, use a mutex object.

A critical section object cannot be moved or copied. The process must also not modify the object, but must treat it as logically opaque. Use only the critical section functions to manage critical section objects. When you have finished using the critical section, call the DeleteCriticalSection function.

A critical section object must be deleted before it can be reinitialized. Initializing a critical section that is already initialized results in undefined behavior.

The spin count is useful for critical sections of short duration that can experience high levels of contention. Consider a worst-case scenario, in which an application on an SMP system has two or three threads constantly allocating and releasing memory from the heap. The application serializes the heap with a critical section. In the worst-case scenario, contention for the critical section is constant, and each thread makes an processing-intensive call to theWaitForSingleObject function. However, if the spin count is set properly, the calling thread does not immediately callWaitForSingleObject when contention occurs. Instead, the calling thread can acquire ownership of the critical section if it is released during the spin operation.

 
  About CriticalSection

在多进程的环境里,需要对线程进行同步.常用的同步对象有临界段(Critical Section),互斥量(Mutex),信号量(Semaphore),事件(Event)等,除了临界段,都是内核对象。
  在同步技术中,临界段(Critical Section)是最容易掌握的,而且,和通过等待和释放内核态互斥对象实现同步的方式相比,临界段的速度明显胜出.但是临界段有一个缺陷,WIN32文档已经说明了临界段是不能跨进程的,就是说临界段不能用在多进程间的线程同步,只能用于单个进程内部的线程同步.
  因为临界段只是一个很简单的数据结构体,在别的进程的进程空间里是无效的。就算是把它放到一个可以多进程共享的内存映象文件里,也还是无法工作.
  有甚么方法可以跨进程的实现线程的高速同步吗?
  2.原理和实现
  2.1为什么临界段快? 是“真的”快吗?
  确实,临界段要比其他的核心态同步对象要快,因为EnterCriticalSection和LeaveCriticalSection这两个函数从InterLockedXXX系列函数中得到不少好处(下面的代码演示了临界段是如何使用InterLockedXXX函数的)。InterLockedXXX系列函数完全运行于用户态空间,根本不需要从用户态到核心态
  之间的切换。所以,进入和离开一个临界段一般只需要10个左右的CPU执行指令。而当调用WaitForSingleObject之流的函数时,因为使用了内核对象,线程被强制的在用户态和核心态之间变换。在x86处理器上,这种变换一般需要600个CPU指令。看到这里面的巨大差距了把。
  话说回来,临界段是不是真正的“快”?实际上,临界段只在共享资源没有冲突的时候是快的。当一个线程试图进入正在被另外一个线程拥有的临界段,即发生竞争冲突时,临界段还是等价于一个event核心态对象,一样的需要耗时约600个CPU指令。事实上,因为这样的竞争情况相对一般的运行情况来说是很少的(除非人为),所以在大部分的时间里(没有竞争冲突的时候),临界段的使用根本不牵涉内核同步,所以是高速的,只需要10个CPU的指令。(bear说:明白了吧,纯属玩概率,Ms的小花招)
  2.3进程边界怎么办?
  “临界段等价于一个event核心态对象”是什么意思?
  看看临界段结构的定义

  

typedef struct _RTL_CRITICAL_SECTION {  PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
  //  // The following three fields control entering and exiting the critical
  // section for the resource  //  LONG LockCount;  LONG RecursionCount;
  HANDLE OwningThread; // from the thread's ClientId->UniqueThread
  HANDLE LockSemaphore;  DWORD SpinCount;
  } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
  #typedef RTL_CRITICAL_SECTION CRITICL_SECTION

DebugInfo 此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为RTL_CRITICAL_SECTION_DEBUG。这一结构中包含更多极有价值的信息,也定义于 WINNT.H 中。我们稍后将对其进行更深入地研究。

LockCount 这是临界区中最重要的一个字段。它被初始化为数值 -1;此数值等于或大于 0 时,表示此临界区被占用。当其不等于 -1 时,OwningThread 字段(此字段被错误地定义于 WINNT.H 中 — 应当是 DWORD 而不是 HANDLE)包含了拥有此临界区的线程 ID。此字段与 (RecursionCount -1) 数值之间的差值表示有多少个其他线程在等待获得该临界区。

RecursionCount 此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。

OwningThread 此字段包含当前占用此临界区的线程的线程标识符。此线程 ID 与 GetCurrentThreadId 之类的 API 所返回的 ID 相同。

LockSemaphore
此字段的命名不恰当,它实际上是一个自复位事件,而不是一个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用DeleteCriticalSection(它将发出一个调用该事件的 CloseHandle 调用,并在必要时释放该调试结构),否则将会发生资源泄漏。

SpinCount 仅用于多处理器系统。MSDN 文档对此字段进行如下说明:“在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转 dwSpinCount 次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。”旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用InitializeCriticalSectionAndSpinCount API 将其设置为一个不同值。

  在CRITICAL_SECTION 数据结构里,有一个Event内核对象的句柄(那个undocument的结构体成员LockSemaphore,包含的实际是一个event的句柄, 而不是一个信号量semaphore)。正如我们所知,内核对象是系统全局的,但是该句柄是进程所有的,而不是系统全局的。所以,就算把一个临界段结构直接放到共享的内存映象里,临界段也无法起作用,因为LockSemaphore里句柄值只对一个进程有效,对于别的进程是没有意义的。 在一般的进程同步中,进程要使用一个别的进程创建的的Event 对象,必须调用OpenEvent或CreaetEvent函数来得到进程可以使用的句柄值。

CRITICAL_SECTION结构里其他的变量是临界段工作所依赖的元素,Ms也“警告”程序员不要自己改动该结构体里变量的值。是怎么实现的呢?看下一步.
  2.4 COptex,优化的同步对象类
  Jeffrey Richter曾经写过一个自己的临界段,现在,他把他的临界段改良了一下,把它封装成一个COptex类。成员函数TryEnter拥有NT4里介绍的函数TryEnterCriticalSection的功能,这个函数尝试进入临界段,如果失败立刻返回,不会挂起线程,并且支持Spin计数.这个功能在NT4/SP3中被InitializeCriticalSectionAndSpinCount 和SetCriticalSectionSpinCount实现。Spin计数在多处理器系统和高竞争冲突情况下是很有用的,在进入WaitForXXX核心态之前,临界段根据设定的Spin计数进行多次TryEnterCtriticalSection,然后才进行堵塞。想一下,TryEnterCriticalSection才使用10个左右的周期,如果在Spin计数消耗完之前,冲突消失,临界段对象是空闲的,那么再用10个CPU周期就可以在用户态进入临界段了,不用切换到核心态.
  (bear说:为了避免这个"核心态",Ms自己也是费劲脑汁呀.看出来了吧,优化的原则:在需要的时候才进入核心态。否则,在用户态进行同步)

 

Mutex 分为递归(recursive) 和非递归(non-recursive)两种,这是POSIX 的叫法,另外的名字是可重入(Reentrant) 与非可重入。这两种mutex 作为线程间(inter-thread) 的同步工具时没有区别,它们的惟一区别在于:同一个线程可以重复对recursive mutex 加锁,但是不能重复对non-recursive mutex 加锁。

首选非递归mutex,绝对不是为了性能,而是为了体现设计意图。non-recursive 和recursive 的性能差别其实不大,因为少用一个计数器,前者略快一点点而已。在同一个线程里多次对non-recursive mutex 加锁会立刻导致死锁,我认为这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。

毫无疑问recursive mutex 使用起来要方便一些,因为不用考虑一个线程会自己把自己给锁死了,我猜这也是Java 和Windows 默认提供recursive mutex 的原因。(Java 语言自带的intrinsic lock 是可重入的,它的concurrent 库里提供ReentrantLock,Windows的CRITICAL_SECTION 也是可重入的。似乎它们都不提供轻量级的non-recursive mutex。)正因为它方便,recursive mutex 可能会隐藏代码里的一些问题。典型情况是你以为拿到一个锁就能修改对象了,没想到外层代码已经拿到了锁,正在修改(或读取)同一个对象呢。具体的例子:

std::vector<Foo> foos;

MutexLock mutex;

void post(const Foo& f)

{

MutexLockGuard lock(mutex);

foos.push_back(f);

}

void traverse()

{

MutexLockGuard lock(mutex);

for (auto it = foos.begin(); it != foos.end(); ++it) { // 用了0x 新写法

it->doit();

}

}

post() 加锁,然后修改foos 对象; traverse() 加锁,然后遍历foos 数组。将来有一天,Foo::doit() 间接调用了post() (这在逻辑上是错误的),那么会很有戏剧性的:

1. Mutex 是非递归的,于是死锁了。

2. Mutex 是递归的,由于push_back 可能(但不总是)导致vector 迭代器失效,程序偶尔会crash。这时候就能体现non-recursive 的优越性:把程序的逻辑错误暴露出来。死锁比较容易debug,把各个线程的调用栈打出来((gdb) thread apply all bt),只要每个函数不是特别长,很容易看出来是怎么死的。( 另一方面支持了函数不要写过长。) 或者可以用PTHREAD_MUTEX_ERRORCHECK 一下子就能找到错误(前提是MutexLock 带debug 选项。)程序反正要死,不如死得有意义一点,让验尸官的日子好过些。

如果一个函数既可能在已加锁的情况下调用,又可能在未加锁的情况下调用,那么就拆成两个函数:

1. 跟原来的函数同名,函数加锁,转而调用第2 个函数。

2. 给函数名加上后缀WithLockHold,不加锁,把原来的函数体搬过来。

就像这样:

void post(const Foo& f)

{

MutexLockGuard lock(mutex);

postWithLockHold(f); // 不用担心开销,编译器会自动内联的

}

// 引入这个函数是为了体现代码作者的意图,尽管push_back 通常可以手动内联

void postWithLockHold(const Foo& f){

foos.push_back(f);

}

这有可能出现两个问题(感谢水木网友ilovecpp 提出):a) 误用了加锁版本,死锁了。b) 误用了不加锁版本,数据损坏了。

对于a),仿造前面的办法能比较容易地排错。对于b),如果pthreads 提供isLocked()就好办,可以写成:

void postWithLockHold(const Foo& f)

{

assert(mutex.isLocked()); // 目前只是一个愿望

// ...

}

另外,WithLockHold 这个显眼的后缀也让程序中的误用容易暴露出来。C++ 没有annotation,不能像Java 那样给method 或field 标上@GuardedBy 注解,需要程序员自己小心在意。虽然这里的办法不能一劳永逸地解决全部多线程错误,但能帮上一点是一点了。我还没有遇到过需要使用recursive mutex 的情况,我想将来遇到了都可以借助wrapper 改用non-recursive mutex,代码只会更清晰。

About Spinlock

spinlock又称自旋锁,线程通过busy-wait-loop的方式来获取锁,任何时刻时刻只有一个线程能够获得锁,其他线程忙等待直到获得锁。spinlock在多处理器多线程环境的场景中有很广泛的使用,一般要求使用spinlock的临界区尽量简短,这样获取的锁可以尽快释放,以满足其他忙等的线程。Spinlock和mutex不同,spinlock不会导致线程的状态切换(用户态->内核态),但是spinlock使用不当(如临界区执行时间过长)会导致cpu busy飙高。

2, spinlock与mutex对比

2.1,优缺点比较

spinlock不会使线程状态发生切换,mutex在获取不到锁的时候会选择sleep

mutex获取锁分为两阶段,第一阶段在用户态采用spinlock锁总线的方式获取一次锁,如果成功立即返回;否则进入第二阶段,调用系统的futex锁去sleep,当锁可用后被唤醒,继续竞争锁。

Spinlock优点:没有昂贵的系统调用,一直处于用户态,执行速度快

Spinlock缺点:一直占用cpu,而且在执行过程中还会锁bus总线,锁总线时其他处理器不能使用总线

Mutex优点:不会忙等,得不到锁会sleep

Mutex缺点:sleep时会陷入到内核态,需要昂贵的系统调用

2.2,使用准则

Spinlock使用准则:临界区尽量简短,控制在100行代码以内,不要有显式或者隐式的系统调用,调用的函数也尽量简短。例如,不要在临界区中调用read,write,open等会产生系统调用的函数,也不要去sleep;strcpy,memcpy等函数慎用,依赖于数据的大小。

3, spinlock系统实现

spinlock的实现方式有多种,但是思想都是差不多的,现罗列一下:

3.1,glibc-2.9中的实现方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

int pthread_spin_lock (lock) pthread_spinlock_t *lock;

asm ("\n"

"1:\t" LOCK_PREFIX "decl %0\n\t"

"jne 2f\n\t"

".subsection 1\n\t"

".align 16\n"

"2:\trep; nop\n\t"

"cmpl $0, %0\n\t"

"jg 1b\n\t"

"jmp 2b\n\t"

".previous"

: "=m" (*lock)

: "m" (*lock));

return 0;

执行过程:

1,lock_prefix 即 lock。lock decl %0,锁总线将%0(即lock变量)减一。Lock可以保证接下来一条指令的原子性

2, 如果lock=1,decl的执行结果为lock=0,ZF标志位为1,直接跳到return 0;否则跳到标签2。也许要问,为啥能直接跳到return 0呢?因为subsection和previous之间的代码被编译到别的段中,因此jne之后紧接着的代码就是 return 0 (leaveq;retq)。Rep nop在经过编译器编译之后被编译成 pause。

3, 如果跳到标签2,说明获取锁不成功,循环等待lock重新变成1,如果lock为1跳到标签1重新竞争锁。

该实现采用的是AT&T的汇编语法,更详细的执行流程解释可以参考“五竹”大牛的文档

3.2,系统自带(glibc-2.3.4)spinlock反汇编代码:

系统环境:

2.6.9-89.ELsmp #1 SMP  x86_64 x86_64 x86_64 GNU/Linux

(gdb) disas pthread_spin_lock

Dump of assembler code for function pthread_spin_lock:

//eax寄存器清零,做返回值

0x0000003056a092f0 <pthread_spin_lock+0>:       xor    %eax,%eax

//rdi存的是lock锁地址,原子减一

0x0000003056a092f2 <pthread_spin_lock+2>:       lock decl (%rdi)

//杯了个催的,加锁不成功,跳转,开始busy wait

0x0000003056a092f5 <pthread_spin_lock+5>:       jne    0x3056a09300 <pthread_spin_lock+16>

//终于夹上了…加锁成功,返回

0x0000003056a092f7 <pthread_spin_lock+7>:       retq

……………………………………….省略若干nop……………………………………….

0x0000003056a092ff <pthread_spin_lock+15>:      nop

//pause指令降低CPU功耗

0x0000003056a09300 <pthread_spin_lock+16>:      pause

//检查锁是否可用

0x0000003056a09302 <pthread_spin_lock+18>:      cmpl   $0×0,(%rdi)

//回跳,重新锁总线获取锁

0x0000003056a09305 <pthread_spin_lock+21>:      jg     0x3056a092f2 <pthread_spin_lock+2>

//长夜漫漫,爱上一个不回家的人,继续等~

0x0000003056a09307 <pthread_spin_lock+23>:      jmp    0x3056a09300 <pthread_spin_lock+16>

0x0000003056a09309 <pthread_spin_lock+25>:      nop

……………………………………….省略若干nop……………………………………….

End of assembler dump.

 

Glibc的汇编代码还是很简洁的,没有多余的代码。

Pause指令解释(from intel):

Description

Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.

提升spin-wait-loop的性能,当执行spin-wait循环的时候,笨死和小强处理器会因为在退出循环的时候检测到memory order violation而导致严重的性能损失,pause指令就相当于提示处理器哥目前处于spin-wait中。在绝大多数情况下,处理器根据这个提示来避免violation,藉此大幅提高性能,由于这个原因,我们建议在spin-wait中加上一个pause指令。

名词解释(以下为本人猜想):memory order violation,直译为-内存访问顺序冲突,当处理器在(out of order)乱序执行的流水线上去内存load某个内存地址的值(此处是lock)的时候,发现这个值正在被store,而且store本身就在load之前,对于处理器来说,这就是一个hazard,流水流不起来。

在本文中,具体是指当一个获得锁的工作线程W从临界区退出,在调用unlock释放锁的时候,有若干个等待线程S都在自旋检测锁是否可用,此时W线程会产生一个store指令,若干个S线程会产生很多load指令,在store之后的load指令要等待store在流水线上执行完毕才能执行,由于处理器是乱序执行,在没有store指令之前,处理器对多个没有依赖的load是可以随机乱序执行的,当有了store指令之后,需要reorder重新排序执行,此时会严重影响处理器性能,按照intel的说法,会带来25倍的性能损失。Pause指令的作用就是减少并行load的数量,从而减少reorder时所耗时间。

An additional function of the PAUSE instruction is to reduce the power consumed by a Pentium 4 processor while executing a spin loop. The Pentium 4 processor can execute a spin-wait loop extremely quickly, causing the processor to consume a lot of power while it waits for the resource it is spinning on to become available. Inserting a pause instruction in a spin-wait loop greatly reduces the processor’s power consumption.

Pause指令还有一个附加所用是减少笨死处理器在执行spin loop时的耗电量。当笨死探测锁是否可用时,笨死以飞快的速度执行spin-wait loop,导致消耗大量电量。在spin-wait loop中插入一条pause指令会显著减少处理器耗电量。

This instruction was introduced in the Pentium 4 processors, but is backward compat­ible with all IA-32 processors. In earlier IA-32 processors, the PAUSE instruction operates like a NOP instruction. The Pentium 4 and Intel Xeon processors implement the PAUSE instruction as a pre-defined delay. The delay is finite and can be zero for some processors. This instruction does not change the architectural state of the processor (that is, it performs essentially a delaying no-op operation).

该指令在笨死中被引入,但是向后兼容所有IA-32处理器,在早期的IA-32处理器中,pause指令以nop的方式来运行,笨死和小强以一个预定义delay的方式来实现pause,该延迟是有限的,在某些处理器上可能是0,pause指令并不改变处理器的架构(位?)状态(也就是说,它是一个延迟执行的nop指令)。至于Pause指令的延迟有多大,intel并没有给出具体数值,但是据某篇文章给出的测试结果,大概有38~40个clock左右(官方数字:nop延迟是1个clock),一下子延迟40个clock,intel也够狠的。

This instruction’s operation is the same in non-64-bit modes and 64-bit mode.

该指令在64位与非64位模式下的表现是一致的。

5, spinlock改进

根据上一小节的分析,pause指令最主要的作用是减低功耗和延迟执行下一条指令。所以我们可以有这样的猜想:如果在spin-wait的过程中,记录下加锁失败的次数,对失败的加锁行为进行惩罚(failure penalty),让等待时间和失败次数成正比,即失败次数越多等待时间越长、执行的pause指令越多。

  • 这样的好处有:

A,   在锁竞争很激烈的情况下,通过增加延迟来降低锁总线的次数,当锁总线次数降低时,系统其它程序对内存的竞争减少,提高系统整体吞吐量

B,   在锁竞争很激烈的情况下,减少计算指令的执行次数,降低功耗,低碳低碳!!!

C,   当锁竞争不激烈时,还能获得和原来一样的性能

  • 但是带来的问题有:

A,   如果竞争锁失败次数越多,pause次数越多的话,会导致有些线程产生starvation

B,   在某些特殊的场景下,锁竞争很小的时候,failure penalty可能会导致程序执行时间变长,进而导致总的加锁次数不一定减少

  • 解决方案:

当失败次数超过一个阈值的时候,将失败次数清零,使线程以洁白之躯重新参与竞争;对于少数failure penalty导致

 

Pthreads mutex VS Pthreads spinlock

来自:http://www.searchtb.com/2011/01/pthreads-mutex-vs-pthread-spinlock.html

锁机制(lock) 是多线程编程中最常用的同步机制,用来对多线程间共享的临界区(Critical Section) 进行保护。

Pthreads提供了多种锁机制,常见的有:
1) Mutex(互斥量):pthread_mutex_***
2) Spin lock(自旋锁):pthread_spin_***
3) Condition Variable(条件变量):pthread_con_***
4) Read/Write lock(读写锁):pthread_rwlock_***

在多线程编中,根据应用场合的不同,选择合适的锁来进行同步,对多线程程序的性能影响非常大. 本文主要对 pthread_mutex 和 pthread_spinlock 两种锁制机进行比较,并讨论其适用的场合.

1 Pthread mutex

Mutex属于sleep-waiting类型的锁. 从 2.6.x 系列稳定版内核开始, Linux 的 mutex 都是 futex (Fast-Usermode-muTEX)锁.
futex(快速用户区互斥的简称)是一个在Linux上实现锁定和构建高级抽象锁如信号量和POSIX互斥的基本工具。它们第一次出现在内核开发的2.5.7版;其语义在2.5.40固定下来,然后在2.6.x系列稳定版内核中出现。
Futex 是由Hubertus Franke(IBM Thomas J. Watson 研究中心), Matthew Kirkwood,Ingo Molnar(Red Hat)和 Rusty Russell(IBM Linux 技术中心)等人创建的。
Futex 是由用户空间的一个对齐的整型变量和附在其上的内核空间等待队列构成. 多进程或多线程绝大多数情况下对位于用户空间的futex 的整型变量进行操作(汇编语言调用CPU提供的原子操作指令来增加或减少),而其它情况下,则需要通过代价较大的系统调用来对位于内核空间的等待队列进行操作(如唤醒等待的进程/线程,或 将当前进程/线程放入等待队列). 除了多个线程同时竞争锁的少数情况外,基于 futex 的 lock 操作是不需要进行代价昂贵的系统调用操作的.
.
这种机制的核心思想是通过将大多数情况下非同时竞争 lock 的操作放到在用户空间来执行,而不是代价昂贵的内核系统调用方式来执行,从而提高了效率.

Pthreads提供的Mutex锁操作相关的API主要有:
1、 pthread_mutex_lock (pthread_mutex_t *mutex);
2、 pthread_mutex_trylock (pthread_mutex_t *mutex);
3、 pthread_mutex_unlock (pthread_mutex_t *mutex);

因为源代码比较长,这里不做摘录,大家可以参考:
glibc-2.12.2/nptl/pthread_mutex_lock.c

2 Pthread spinlock

spinlock,也称自旋锁,是属于busy-waiting类型的锁.在多处理器环境中, 自旋锁最多只能被一个可执行线程持有。如果一个可执行线程试图获得一个被争用(已经被持有的)自旋锁,那么该线程就会一直进行忙等待,自旋,也就是空转,等待锁重新可用。如果锁未被争用,请求锁的执行线程便立刻得到它,继续执行。

一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,特别的浪费CPU时间,所以自旋锁不应该被长时间的持有。实际上,这就是自旋锁的设计初衷,在短时间内进行轻量级加锁。

Kernel中的自旋锁不能够在能够导致睡眠的环境中使用。举个例子,一个线程A获得了自旋锁L;这个时候,发生了中断,在对应的中断处理函数B中,也尝试获得自旋锁L,就会中断处理程序进行自旋。但是原先锁的持有者只有在中断处理程序结束后,采用机会释放自旋锁,从而导致死锁。
由于涉及到多个处理器环境下,spin lock的效率非常重要。因为在等待spin lock的过程,处理器只是不停的循环检查,并不执行其他指令。但即使这样, 一般来说,spinlock的开销还是比进程调度(context switch)少得多。这就是spin lock 被广泛应用在多处理器环境的原因

Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);

下面,来看一下spinlock在pthread中的实现:

1) spin lock的数据结构

glibc-2.12.2\nptl\sysdeps\unix\sysv\linux\i386\bits\pthreadtypes.h

1

typedef volatile int pthread_spinlock_t;

2) pthread_spin_lock

glibc-2.12.2\nptl\sysdeps\i386\pthread_spin_lock.c

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

#ifndef LOCK_PREFIX

# ifdef UP

#  define LOCK_PREFIX    /* nothing */

# else

#  define LOCK_PREFIX    "lock;"

# endif

#endif

  

int

pthread_spin_lock (lock)

     pthread_spinlock_t *lock;

{

  asm ("\n"

       "1:\t" LOCK_PREFIX "decl %0\n\t"

       "jne 2f\n\t"

       ".subsection 1\n\t"

       ".align 16\n"

       "2:\trep; nop\n\t"

       "cmpl $0, %0\n\t"

       "jg 1b\n\t"

       "jmp 2b\n\t"

       ".previous"

       : "=m" (*lock)

       : "m" (*lock));

  

  return 0;

}

a、 LOCK_PREFIX: 是为了在SMP下锁总线,保证接下来一条指令的原子性。

b、 %0: 这里是*lock的值,先将lock的值减一,如果ZF=0(lock值不为0),跳到下面的2标签处继续执行;否则执行结束(lock值为0)。
c、 jne: Jump near if not equal (ZF=0). Not supported in 64-bit mode

下面继续看2标签处的代码:
d、 rep; nop: 为实际上为多个nop指令,实际上这条指令可以降低CPU的运行频率,减低电的消耗量,但最重要的是,提高了整体的效率。因为这段指令执行太快的话,会生成很多读取内存变量的指令,另外的一个CPU可能也要写这个内存变量,现在的CPU经常需要重新排序指令来提高效率,如果读指令太多的话,为了保证指令之间的依赖性,CPU会以牺牲流水线执行(pipeline)所带来的好处。从pentium 4以后,intel引进了一条pause指令,专门用于spin lock这种情况,据intel的文档说,加上pause可以提高25倍的效率!。
e、 cmpl $0, %0 :比较lock与0的大小,当发现Lock大于0的时候,跳回到1标签,尝试重新获得锁;否则,跳回到标签2继续进行循环。

f、 标签1处的代码,在尝试获得锁的时候,直接将lock值减1,如果获得锁操作失败的时候,实际上lock值已经被减了1。这样会不会有问题呢?实际上,这个问题不用担心,因为在释放锁的时候,lock的值还会被重新设置为1。

.subsection和.previous之间的这段代码用来检测spin lock何时被释放. 这段代码与其它的常用指令代码并不是放在同一个代码段中的,因为大部分情况下,lock都会成功返回,将这段lock失败后的操作代码与其它的代码分开,会提高高速缓存的效率(有限的高速缓存可以放置更多的数据)。

3) pthread_spin_unlock

glibc-2.12.2\nptl\sysdeps\i386\pthread_spin_unlock.S

1

2

3

4

5

6

7

8

9

10

11

12

13

.globl    pthread_spin_unlock

    .type    pthread_spin_unlock,@function

    .align    16

pthread_spin_unlock:

    movl    4(%esp), %eax

    movl    $1, (%eax)

    xorl    %eax, %eax

    ret

    .size    pthread_spin_unlock,.-pthread_spin_unlock

  

    /* The implementation of pthread_spin_init is identical.  */

    .globl    pthread_spin_init

pthread_spin_init = pthread_spin_unlock

pthread_spin_unlock()就简单很多了,只是简单的将lock值设置为1,并返回0

 
 
 
 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值