用户模式的线程同步
来源:互联网 发布:淘宝网优衣库旗舰店 编辑:程序博客网 时间:2024/05/16 05:12
在以下两种基本情况下,线程之间需要相互通信:
- 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性;
- 一个线程需要通知其它线程某项任务已经完成。
1.原子访问:Interlocked系列函数
所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。
我们需要有一种方法能够保证对一个值的递增操作时原子操作——也就是说,不会被打断。Interlocked系列函数提供了我们需要的解决方案。
LONG
PLONG volatileplAddend,
LONGlIncrement);
LONGLONG
PLONGLONG volatilepllAddend,
LONGLONGllIncrement);
只要调用这个函数,传一个长整形变量的地址和另一个增量值,函数就会保证递增操作是以原子方式进行的。
Interlocked函数又是如何工作的呢?取决于代码运行的CPU平台。如果是x86系列CPU,那么Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其它CPU访问同一个内存地址。无论编译器如何生成代码,无论机器上装配了多少个CPU,这些函数都能够保证对值的修改时以原子方式进行的。
Interlocked函数执行得极快,调用一次Interlocked函数,通常只占用几个CPU周期(通常小于50),而且也不需要在用户模式和内核模式之间进行切换(这个切换通常需要占用1000个周期以上)。
当然,也可以用InterlockedExchangeAdd来做减法——只要在第二个参数中传入一个负值就行了。
下面是其它三个Interlocked函数:
LONG
PLONG volatileplTarget,
LONGlValue);
LONGLONG
PLONGLONG volatileplTarget,
LONGLONGlValue);
PVOID
PVOID* volatileppvTarget,
PVOID pvValue);
实现自旋锁的时候,InterlockedExchange及其有用:
// Global variable indicatingwhether a shared resource is in use ornot
BOOL g_fResourceInUse =FALSE;
void Func1(){
// Wait to access theresource.
while (InterlockedExchange
Sleep(0);
// Access the resource.
...
// We no longer need to access theresource.
InterlockedExchange(&g_fResourceInUse,FALSE);
}
在单CPU的机器上应避免使用旋转锁
PVOID
PLONGplDestination,
LONGlExchange,
LONGlComparand);
PVOID
PVOID*ppvDestination,
PVOIDpvExchange,
PVOIDpvComparand);
该函数对当前值(
2.高级线程同步
如果我们只需要以原子方式修改一个值,那么Interlocked系列函数非常好用,我们当然应该优先使用它们。为了能够以“原子”方式访问复杂的数据结构,我们必须超越Interlocked系列函数。
我们既不应该使用旋转锁,也不应该进行轮循,因为浪费CPU时间是件很糟糕的事情。而应该调用函数把线程切换到等待状态,直到线程想要访问的资源可供使用为止。
volatile关键字:
volatile限定符告诉编译器这个变量可能被应用程序之外的其它东西修改,比如操作系统、硬件或者一个并发执行的线程。确切地说,volatile限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。给一个结构加volatile限定符等于给结构中所有的成员都加volatile限定符,这样可以确保任何一个成员始终都是从内存中读取的。
3.关键段
关键段
一般情况下,我们会将CRITICAL_SECTION结构作为全局变量来分配,这样进程中的所有线程就能够非常方便地通过变量名来访问这些结构。在使用CRIICAL_SECTION的时候,只有两个必要条件:第一条件是所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址(我们可以通过自己喜欢的任何方式来把这个地址传给各个线程)。第二个条件是在任何线程试图访问被保护的资源之前,必须对CRITICAL_SECTION结构的内部成员进行初始化。
下面这个函数用来对结构进行初始化:
VOID
当知道线程不再需要访问共享资源的时候,我们应该调用下面的函数来清理CRITICAL_SECTION结构:
VOID
然后我们在以下两个函数之间访问共享资源:
VOID
。。。共享资源的访问。。。
VOID
EnterCriticalSection会执行下面的测试:
- 如果没有线程正在访问该资源,
EnterCriticalSection便更新成员变量,以表示调用线程已被赋予访问权并立即返回,使该线程能够继续运行(访问该资源)。
- 如果成员变量表明调用线程已经被赋予对资源的访问权,那么
EnterCriticalSection便更新这些变量,以指明调用线程多少次被赋予访问权并立即返回,使该线程能够继续运行。这种情况很少出现,并且只有当线程在一行中两次调用 EnterCriticalSection而不影响对LeaveCriticalSection的调用时,才会出现这种情况。
- 如果成员变量指明,一个线程(除了调用线程之外)已被赋予对资源的访问权,那么EnterCriticalSection将调用线程置于等待状态。这种情况是极好的,因为等待的线程不会浪费任何CPU时间。系统能够记住该线程想要访问该资源并且自动更新
CRITICAL_SECTION 的成员变量,一旦目前访问该资源的线程调用 LeaveCriticalSection函数,该线程就处于可调度状态。
我们可以用下面的函数的函数来代替EnterCriticalSection:
BOOL
TryEnterCriticalSection从来不会让调用线程进入等待状态。
当不能用Interlocked函数解决同步问题的时候,我们应该试一试关键段。关键段的最大好处在于它们非常容易使用,而且它们在内部也使用了Interlocked函数,因此执行速度非常快。关键段的最大缺点在于它们无法用来在多个进程之间对线程进行同步。
当线程试图进入一个关键段,但这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销非常大。为了提高关键段的性能,Microsoft把旋转锁合并到了关键段中。因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权。只有尝试失败的时候,线程才会切换到内核模式并进入等待状态。
为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段:
BOOL
PCRITICAL_SECTIONpcs,
DWORDdwSpinCount);
dwSpinCount是我们希望旋转锁循环的次数。
我们可以调用一下函数来改变关键段的旋转次数:
DWORD
PCRITICAL_SECTIONpcs,
DWORDdwSpinCount);
用来保护进程堆的关键段锁使用的旋转次数大约是4000,这可以作为我们的一个参考值。
4.Slim读/写锁
SRWLock的目的和关键段相同:对一个资源进行保护,不让其它线程访问它。但是,与关键段不同的是,SRWLock允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有的读取者线程在同一时刻访问共享资源应该是可行的,这是因为仅仅读取资源的值并不存在破坏数据的风险。只有当写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下,写入者线程想要对资源进行更新的时候才需要进行同步。在这种情况下,写入者线程应该独占对资源的访问权:任何其它线程,无论是读取者线程还是写入者线程,都不允许访问资源。这就是SRWLock提供的全部功能。
首先,我们需要分配一个SRWLOCK结构并用InitializeSRWLock函数对它进行初始化:
VOID
一旦SRWLock初始化完成之后,写入者线程就可以调用AcquireSRWLockExclusive,将SRWLOCK对象的地址作为参数传入,以尝试获得对被保护资源的独占访问权。
VOID
完成对资源的更新之后,应该调用ReleaseSRWLockExclusice,并将SRWLOCK对象的地址作为参数传入,这样就解除了对资源的锁定。
VOID
对读取者线程来说,同样有两个步骤,但调用的是下面两个新的函数:
VOID
VOID
不存在用来删除或销毁SRWLOCK的函数,系统会自动执行清理工作。
与关键段相比,SRWLock缺乏下面两个特性:
- 不存在TryEnter(Shared/Exclusive)SRWLock
之类的函数:如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive) 会阻塞调用线程。 - 不能递归地调用SRWLOCK。也就是说,一个线程不能为了多次写入资源而多次锁定资源,然后再多次调用ReleaseSRWLock*
来释放对资源的锁定。
线程/微妙
Volatile
读取
Volatile
写入
Interlocked
递增
Critical Section
关键段
SRWLock
共享模式
SRWLock
独占模式
Mutex
互斥量
1
8
8
35
66
66
67
1060
2
8
76
153
268
134
148
11082
4
9
145
361
768
244
307
23785
表1:同步机制的性能比较
总结一下,如果希望在应用程序中得到最佳性能,那么首先应该尝试不要共享数据,然后依次使用volatile读取,volatile写入,InterlockedAPI,SRWLock以及关键段。当且仅当所有这些都不能满足要求的时候,再使用内核对象。因为每次等待和释放内核对象都需要在用户模式和内核模式之间切换,这种切换的CPU开销非常大。
5.一些有用的窍门和技巧
- 以原子方式操作一组对象时使用一个锁;
- 同时访问多个逻辑资源时以完全相同的顺序来获得资源的锁;
- 不要长时间占用锁;
- 用户模式的线程同步
- 线程同步(1) - 用户模式下的线程同步
- Windows线程同步—用户模式下的线程同步
- 线程同步(1) - 用户模式下的线程同步
- 线程同步——用户模式下的线程同步
- 用户模式下的线程同步
- 用户模式下的线程同步
- Chapter08-用户模式下的线程同步
- 用户模式下的线程同步
- 八、 用户模式下的线程同步
- 八 用户模式下的线程同步
- 用户模式下的线程同步
- Chapter08-用户模式下的线程同步
- 用户模式下的线程同步
- 用户模式下的线程同步
- 用户模式下线程同步
- 用户模式下线程同步
- 线程同步 总结 用户模式同步对象
- 提供最好的服务
- windows进程间通信的4种基本方法
- 邮槽通信
- 作业
- 内核对象的线程同步
- 用户模式的线程同步
- Lowest Common Ancestor问题的解决思路
- 伪句柄
- Windows的消息分流器
- 提升进程权限
- CreateProcess函数
- 跨进程边界共享内核对象
- 安全函数(后缀为_s)的参数检查和…
- 最长公共子序列