分类: Windows平台
2014-01-03 21:00:12
所谓原子访问,指的是一个线程在访问某个资源的同时能保证没有其它线程会在同一时刻访问同一资源。有下列函数
InterlockedExchangeAdd
InterlockedExchangeAdd64
InterlockedExchange
InterlockedExchangePointer
InterlockedCompareExchange
InterlockedCompareExchangePointer
InterlockedIncrement
InterlockedDecrement
另外还有一组基于InterlockedCompareExchange64的OR,AND和XOR辅助函数
InterlockedAnd64
InterlockedOr64
InterlockedXor64
从XP开始,除了能对整数或布尔值进行这些原子操作外,我们还能使用一系列其它的函数来对一种被称为Interlocked单向链表的栈进行操作。
InitializeSListHead
InterlockedPushEntrySList
InterlockedPopEntrySList
InterlockedFlushSList
QueryDepthSList
如果想确定CPU的调整缓存行的大小,可以使用GetLogicalProcessorInformation函数,这个函数会返回一个SYSTEM_LOGICAL_PROCESSOR_INFORMATION结构数组,我们可以检查每个结构的Cache字段,该成员是一个CACHE_DESCRIPTOR结构,其中的LineSize字段表示CPU的高速缓存行的大小。一旦有了这一信息,我们就可以使用C/C++编译器的__declspec(align(#))指示符来对字段对齐加以控制。
volatile类型限定符告诉编译器这个变量可能会被应用程序之外的其它东西修改,比如操作系统、硬件或者一个并发执行的线程。确切的说,volatile限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量的内存中的位置读取变量的值。另外,给一个结构加上volatile限定符等于给结构中所有的成员都加volatile限定符,这样可以确保任何一个成员始终都是从内存中读取的。
关键段
使用CRITICAL_SECTION定义一个关键段变量后,即可使用EnterCriticalSection进入关键段,使用LeaveCriticalSection离开关键段,这两个函数使用的均是关键段变量的地址。在使用CRITICAL_SECTION结构的地址时只有两个必要条件,第一个条件是所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址,我们可以通过自己喜欢的任何方式来把这个地址传给各个线程。第二个条件是在任何线程试图访问被保护的资源之前,必须对CRITICAL_SECTION结构的内部成员进行初始化,InitializeCriticalSection用来对结构进行初始化。当知道线程将不再需要访问共享资源的时候,我们应该调用函数DeleteCriticalSection来删除一个关键段。
当我们不想让调用线程在关键段不可入的时候进入等待状态的话,也可以用函数TryEnterCriticalSection,它从来不会让主调线程进入等待状态,它会通过返回值来表示调用线程是否获准访问资源,因此,如果TryEnterCriticalSection发现资源正在被其它线程访问,那么它他会返回FALSE,否则会返回TRUE,当返回TRUE的时候线程已经进入关键段了,因此每个返回TRUE的TryEnterCriticalSection都必须有一个对应的LeaveCriticalSection。
LeaveCriticalSection调用的时候会检查内部的成员变量,并将计数器减1,该计数器用来表示调用线程获准访问共享资源的次数。如果计数器大于0,LeaveCriticalSection会直接返回,不执行任何其它操作,如果计数器变成了0,LeaveCriticalSection会更新成员变量,以表示没有任何线程正在访问被保护的资源。
由于已经被使用的资源可能在很短的时间内被释放,而线程进入等待状态会消耗大量CPU时间,因此微软把旋转锁合并到关键段中,当调用EnterCriticalSection的时候,它会用一个旋转锁不断的循环,尝试在一段时间内获得对资源的访问权,只有在尝试失败的时候,线程才会切换到内核模式并进入等待状态。可以使用InitializeCriticalSectionAndSpinCount来初始化一个带旋转锁的关键段。使用SetCriticalSectionSpinCount可以改变关键段的旋转次数,注:对于单处理器,旋转次数应该为0。用来保护进程堆的关键段所使用的旋转次数大约是4000,这可以作为我们的一个参考值。
使用关键段的时候还有另一个问题,如果两个或两个以上线程在同一时刻争夺同一个关键段,那么关键段会在内部使用一个事件内核对象,由于争夺现象很少发生,因此只有当第一次要用到事件内核对象的时候,系统才会真正创建它。
SRWLock实现功能类似读者写者问题,即允许许多读取者线程在同一时刻访问共享资源,而只允许一个写入者独占资源,在写入者对资源进行更新的时候才需要进行同步。
首先,我们需要分配一个SRWLOCK结构并用函数InitializeSRWLock对它进行初始化,一旦SRWLock初始化完成之后。写入者线程就可以调用AcquireSRWLockExclusive来尝试获得对被保护的资源的独占访问权,在访问完成后应该调用ReleaseSRWLockExclusive来解除对资源的锁定。
读取者线程可以使用AcquireSRWLockShared和ReleaseSRWLockShared这两个函数来完成对资源的访问。
有时候我们想让线程以原子方式把锁释放并将自己阻塞,直到某个条件成立为止,手动实现这样的线程同步比较复杂。Windows通过函数SleepConditionVariableCS或函数SleepConditionVariableSRW来实现。当另一个线程检测到相应的条件已经满足的时候,比如存在一个元素可让读取者线程读取,或者有足够的空间让写入者线程插入新的元素,它会调用WakeConditionVariable或WakeAllConditionVariable,这样阻塞在Sleep*函数中的线程就会被唤醒。
以原子方式操作一组对象时使用一个锁,一种常见情况是多个对象聚在一起会构成一个单独的“逻辑”资源。为此我们在对这个逻辑资源进行读操作或者写操作,都应该只使用一个锁。
同时访问多个逻辑资源。有时我们需要同时访问多个逻辑资源,例如,应用程序可能需要锁定一个资源来取出一个元素,同时锁定另一个资源来把元素加入其中。如果每个资源都有自己的锁,那么我们必须使用所有的锁才能以原子方式完成这个操作。
不要长时间占用锁,如果一个锁被长时间占用,那么其它线程可能会进入等待状态,这会影响到应用程序的性能。比如SendMessage函数可能需要很长时间才能返回,这时候就不能用在关键资源中。