线程同步(VC_Win32)
目录
Interlocked 系列函数
多线程访问共享变量的问题
关键代码段/临界资源
Slim 读写锁
条件变量
线程同步(内核对象)
线程同步的比较
Interlocked 系列函数
为何要使用 Interlocked 系列来修改线程共享变量,先看看下面的例子:
//共享变量 g_x long g_x = 0; //线程1 DWORD WINAPI ThreadFunc1(PVOID pvParam){ g_x++; return 0; } //线程2 DWORD WINAPI ThreadFunc2(PVOID pvParam){ g_x++; return 0; }
代码中声明了一个全局变量并将它初始化为 0.现在假设我们创建了两个线程,一个线程执行 ThreadFunc1,另一个线程执行 ThreadFun2.这两个函数中的代码完全相同:它们都把全局变量 g_x 加 1.因此当两个线程都停止运行的时候,我们可能认为 g_x 的值会是 2.但真的是这样吗?答案是有可能.根据代码编码方式.我们无法确切地知道 g_x 最终会等于几,下面就是原先.假设编译器在编译将 g_x递增的那行代码时,生成了下面的汇编代码:
MOV EAX,[g_x] ; move the value in g_x into a register. INC EAX ; Increment the value in the register. MOV [g_x].EAX ; Store the new value back in g_x.
两线程不太可能在完全相同的时刻执行上面代码.因此如果一个线程先执行,另一个线程随后执行,那么下面将是执行的结果:
;ThreadFunc1: MOV EAX,[g_x] ; move the value in g_x into a register. INC EAX ; Increment the value in the register. MOV [g_x].EAX ; Store the new value back in g_x. ;ThreadFunc2: MOV EAX,[g_x] ; move the value in g_x into a register. INC EAX ; Increment the value in the register. MOV [g_x].EAX ; Store the new value back in g_x.
当两个线程把 g_x 递增的操作完成后, g_x 的值是2.这非常好,和我们预计的完全相同:先等于0,然后加1两次,最终答案是2.漂亮!但等一会,由于 Windows 是一个抢占式的多线程环境,因此系统可能会在任一时刻暂停执行一个线程,切换到另一个线程并让新线程继续执行.因为这个原因,前面的代码可能不会严格按照前面显示的顺序执行,而可能会按照下面顺序执行:
;ThreadFunc1: MOV EAX,[g_x] ; move the value in g_x into a register. INC EAX ; Increment the value in the register. ;ThreadFunc2: MOV EAX,[g_x] ; move the value in g_x into a register. INC EAX ; Increment the value in the register. MOV [g_x].EAX ; Store the new value back in g_x. ;ThreadFunc1: MOV [g_x].EAX ; Store the new value back in g_x.
如果按上面的执行顺序 g_x 最终值会为 1.
为了避免上面的问题,所以所有线程都应该调用 Interlocked 系列函数来修改共享变量,任何一个线程都不应该使用一些简单的 C++ 语句来修改共享变量
Interlocked 运行机制
这些 Interlocked 运行机制取决于运行代码的 CPU 平台.如果是 x86 系列 CPU,那么 Interlocked 函数会在总线上维持一个硬件信号,这个信号会阻止其他 CPU 访问同一个内存地址.而且我们必须确保传给这些函数的变量地址是经过对齐的,否则这些函数可能会失败.C 运行库提供一个 _aligned_malloc 函数,我们可以用这个函数分配得到一块对齐过的内存.
此外我们必须确保锁变量所保护的数据位于不同的高速缓存行中.如果锁变量和数据共享同一个高速缓存行中,那么使用资源的 CPU 就会与任何试图访问资源的 CPU 发送争夺,而这会影响性能.
关于 Interlocked 函数,我们需要知道的另一个要点就是他们执行得极快,调用一次 Interlocked 函数通常只占几个 CPU 周期(通常小于50),而且不需要用户模式和内核模式之间的切换(这个切换通知需要占用 1000 个周期以上)
没有那个 Interlocked 函数可以仅用来读取一个值(但又不修改它),这是因为这个的功能没必要的.
高速缓存行
如果想为装配有多个处理器的机器构建构建高性能应用程序,那么应该注意高速缓存行.当 CPU 从内存中独缺一个字节的时候,它并只是从内存中取回一个字节,而是取回一个高速缓存行.高速缓存行可能包含 32 个字节(老实CPU),64 字节,甚至是 128 字节(取决于CPU),它们始终都对齐到 32 字节边界,64 字节边界,或 128 字节边界
高速缓存行是的内存更新变得更为困难,例如以下的情况
- CPU1 读取一个字节,这使得该字节以及与它相邻的字节被读到 CPU1 的高速缓存行中.
- CPU2 读取同一个字节,这使得该字节被读到 CPU2 的高速缓存行中.
- CPU1 对内存中的这个字节进行修改,这使得该字节被写入到 CPU1 的高速缓存行中.但这一信息没有写到内存.
- CPU2 在次读取同一个字节.由于该字节以及存在 CPU2 的高速缓存行中,因此 CPU2 不需要再访问内存.但 CUP2 将无法看到该字节在内存中的新的值.
这种情况非常糟糕,当然,CPU 芯片的设计者非常清楚这个问题,并做了专门的设计对它进行处理.明确的说,当一个 CPU 修改了高速缓存行中的一个字节的时,机器中的其他 CPU 会接收到通知,并使得自己的高速缓存行作废,因此在刚才的情形中.当 CPU1 修改该字节时, CPU2 的高速缓存行就作废了.在第四步中,CPU1 必须将它的高速缓存行写会内存中,CPU2 必须重新访问内存来填满它的高速缓存行.我们可以看到,虽然高速缓存行能够提高性能,但在多处理机的机器上它们同样能损伤性能
这一切都意味着我们应该根据高速缓存行的大小来将应用程序的数据组织在一起,并将数据与缓存行的边界对齐.这样做的目的是为了确保不同的 CPU 能够各自访问不同的内存地址,而且这些地址不在同一个高速缓存行中.此外,我们应该把只读数据(或不经常读的数据)与读写数据分隔存放,我们还应该把差不多会在同一时间访问的数据组织在一起.
想要确定 CPU 的高速缓存行的大小,最简单的方法是调用 Win32 的 GetLogicalProcessorInformation 函数.这个函数会返回一个 SYSTEM_LOGICAL_PROCESSOR_INFORMATION 结构数组.我们可以检查每个结构的 Cache 字段,该成员有一个 CACHE_DESCRIPTOR 结构,其中 LineSize 字段表示 CPU 的高速缓存行的大小,一旦有了这一信息,我们就可以使用 C/C++ 编译器的 __declspec(align(#)) 指示符来对字符对齐加以控制.
最好是始终只让一个线程访问数据(函数参数和局部变量是确保这一点的最简单方式),或者始终只让一个 CPU 访问数据(使用线程关系,即 thread affinity),只要能做到其中任何一条,就可以完全避免高速缓存行的问题
Interlocked 系列函数(这里列出了只是常用的部分)
- InterlockedExchangeAdd (不仅可以做加法,也可以做减法,即在第二个参数传入负数)
- InterlockedExchange
- InterlockedExchangePointer (根据操作系统的内存读写能力,即32位操作系统替换的是32位的值,即64位操作系统替换的是64位的值)
- InterlockedCompareExchange
- InterlockedCompareExchangePointer
- InterlockedAnd
- InterlockedOr
- InterlockedXor
- InterlockedIncrement
- InterlockedDecrement
volatile 类型限度
在多线程中的共享变量的 volatile 类型限度符的声明是不可或缺的.它告诉编译器这个变量可能被应用程序自我的其他东西修改,比如操作系统,硬件或者一个并发执行的线程.确切地说,volatile 限定符告诉编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值
如果传入一个变量的地址给函数,那么函数必须从内存中读取它的值,编译器的优化程序不会对此产生影响
代码示例(用VS2005调试)
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; //共享变量 g_x volatile long g_x = 0; //线程1 DWORD WINAPI ThreadFunc1(PVOID pvParam){ //g_x++; InterlockedExchangeAdd(&g_x,1); return 0; } //线程2 DWORD WINAPI ThreadFunc2(PVOID pvParam){ //g_x++; InterlockedExchangeAdd(&g_x,1); return 0; } void main() { HANDLE hThread1,hThread2; //创建子线程 hThread1=CreateThread(NULL,0,ThreadFunc1,NULL,0,NULL); hThread2=CreateThread(NULL,0,ThreadFunc2,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); //输出全局比那里 g_x cout<<g_x<<endl; system("pause"); }
运行结果:
多线程访问共享变量的问题
问题:
在多线程访问共享变量的时候,有可能出现一种多个线程同时对同一个变量进行操作,导致变量数据无法正确使用.例如火车售票系统(源至VC++深入详解第十六章),有两个窗口可以进行售票,但是两个窗口如果没有进行适当的通信可能在销售最后一张票的时候发生错误.即一个更早的售出了最后一张票而稍微后的窗口售出了第0张票.下面用一个程序说明此问题.
代码示例(用VS2005调试)
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int tickets=10; void main() { HANDLE hThread1,hThread2; //创建子线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; } return 0; }
运行结果:
为了解决以上问题需要线程同步,即共享变量在同一时刻只有一个线程可以访问,具体实现有互斥对象,事件对象,关键代码段(临界区资源),Slim 读写锁,条件变量
关键代码段/临界资源
定义说明
- 关键代码段是一小段代码(也称为临界区)工作在用户方式下,它在执行之前需要独占对一些共享资源的访问权(在使用多个临界区的时候要注意防止死锁的发生).这种方式可以让多行代码以"原子方式"来对资源进行操控.这里的原子方式.指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源
- 关键代码段非常类似于我们平常使用的公用电话亭,当我们想进入公用电话亭使用电话这种资源的时候,首先需要判断电话亭里时候有人,如果有人正在里面使用电话,那么我们只能在电话亭外等待,当那个人使用完电话,并离开电话亭后,我们才能进入电话亭使用电话这种资源,容易的我们使用完电话亭后,也要离开电话亭,但是有人在使用完电话始终不出来电话亭的时候,即使我们知道他已经没有使用电话这个资源我们也无法进入电话亭使用电话这个资源
注意
- 任何要访问共享资源的代码段,都必须包含在 EntercriticalSection 和 LeaveCriticalSection 之间.如果忘了哪怕是一个地方,共享资源就有可能会破坏.
- 在任何线程试图范围被保护的资源之前,必须对 CRITICAL_SECTION 结构的内部成员进行初始化.
- 不在需要访问共享资源的时候,我们应该调用 DeleteCriticalSection 函数(对应的初始化 CRITICAL_SECTION 结构体的函数为 InitializeCriticalSection)(DeleteCriticalSection 函数会重置结构中的成员变量.如果还有线程正在使用一个关键段,那么当然不应该删除它)来清理 CRITICAL_SECTION 结构.
EntercriticalSection 函数说明
EntercriticalSection 会检查结构中的成员变量,这些变量表示是否有线程正在访问资源,以及哪个线程正在访问资源.EntercriticalSection 会执行以下测试:
-
- EntercriticalSection 会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行.
- 如果成员变量表示调用线程已经获准访问资源,那么 EntercriticalSection 会更新变量,以表示调用线程被获准的访问的次数,并立即返回.这样线程就可以继续执行.这种情况非常少见,只有当线程在调用 EntercriticalSection 之前联系调用 EntercriticalSection 两次以上才会发生.
- 如果成员变量表示有一个(调用线程之外的其他)线程已经获准访问资源,那么 EntercriticalSection 会使用一个事件内核对象来把调用线程切换到等待状态.这样等待中的线程不会浪费任何 CPU 时间!系统会记住这个线程想要访问这个资源,一旦当前正在访问资源的线程调用了 LeaveCriticalSection, 系统会自动更新 CRITICAL_SCTION 的成员变量并将等待中的线程切换会可调度状态.
EntercriticalSection 的内部并不怎么复杂,它只不过执行了一些简单的测试.这个函数的价值在于它能够以原子方式执行所有这些测试,在一台装备了多处理器的机器上,如果两个线程正好在同一个时刻调 EntercriticalSection, 那么函数仍然能正确地执行:一个线程获准访问资源,另一个线程被切换到等待状态.
如果 EntercriticalSection 把一个线程切换到等待状态,那么在很长一段时间内系统可能不会去调度这个线程.事实上,在一个编写的非常糟糕的应用程序中,系统可能再也不会给这个线程调度 CPU 时间了.如果发生这种情况,我们说线程在挨饿.
实际情况是,等到关键段的线程是绝对不会挨饿的.对 EntercriticalSection 的调用最终会超时并引发异常.我们可以将一个调试器连接到引用程序上,来检查哪里出了问题.导致超时的实际长度由厦门这个注册表子项中包含 CriticalSectionTimeout 值决定:HKEY_LOCAL_MACHINE\System\CurrentControlSet\Session Manager,这个值以秒为单位,它的默认值是 2592000 秒,也就是约为 30 天.不要把这值设置的太低(比如小于3秒),否则它会影响到系统中等待关键段的实际通常都超过 3 秒的那些线程和其他应用程序.
TryEnterCriticalSection 与 LeaveCriticalSection 函数说明
TryEnterCriticalSection 从来不会让调用线程进入等待状态.它会通过返回值来表示调用线程是否获准访问资源.因此,如果 TryEnterCriticalSection 发现资源正在被其他线程访问,那么它会返回 FALSE.其他情况下,它会返回 TRUE.通过使用这个函数,线程可以快速地检查它是否能够访问某个共享资源.如果不能访问共享资源,那么它可以继续做其他的事情,而不用等待.如果 TryEnterCriticalSection 返回 TRUE,那么 CRITICAL_SECITON 的成员变量已经更新过了,以表示该线程正在访问资源.因此,每个返回值为 TRUE 的 TryEnterCriticalSection 调用必须有一个对应的 LeaveCriticalSection.
LeaveCriticalSection 会检查结构内部的成员变量并将计数器减 1,该计数器用来表示调用线程获准访问共享资源的次数.如果计数器大于 0,LeaveCriticalSection 会直接返回,不执行任何操作.如果计数器变成了0,LeaveCriticalSection 会更新成员变量.
关键段和旋转锁
为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段 InitializeCriticalSectionAndSpinCount,如果我们在单处理器的机器上调用这个函数,那么函数会忽略 dwSpinCount 参数,因此次数总是为 0,我们可以用 SetCriticalSectionSpinCount 来设置关键段的旋转次数,如果主机只有一个处理器,函数会忽略 swSpinCount 参数.
关键段和错误处理
在使用关键段的时候还可能产生另一个问题.如果有两个或两个以上显存在同一个时刻争夺同一个关键段,那么关键段会在内部使用一个事件内核对象.由于争夺现象很少发生,因此只有当第一次要用到事件内核对象的时候,系统才会真正创建它.因为大多数关键段从来都不会发生争夺现象,所以这样做可以接收大量的系统资源.顺便提一下,只有在调用 DeleteCriticalSection 的时候,系统才会释放这个内核对象,因此,当用完关键段之后,绝对不应该忘记调用 DeleteCriticalSection.
相关函数
- InitializeCriticalSectionAndSpinCount
- InitializeCriticalSection
- SetCriticalSectionSpinCount
- EntercriticalSection
- TryEnterCriticalSection
- LeaveCriticalSection
- DeleteCriticalSection
执行流程
注意死锁
用临界区资源使多线程同步时候要特别注意线程死锁问题,假设程序有两临界资源(g_csA,g_csB)与两个子线程(子线程 A,子线程 B),子线程执行体流程如下图(图1)表示,当子线程 A 先获得临界资源 g_csA 后由于子线程 A 的时间片用完了,所以跳到子线程 B 进行执行,这时 B 将获得临界资源 g_csB,然后由于 A 获得临界资源 g_csA,所以 B 只好等待直至子线程B时间片用完,然后跳到子线程 A 继续执行,但是这时的临界资源 g_csB 已经被子线程 B占有,所以子线程 A 有进行等待直至时间片用完.于是子线程A与子线程B就进入了死锁现象流程如下图所示(图2).
代码示例(用VS2005调试)
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int tickets=10; //声明关键代码量CRITICAL_SECTION结构体变量 CRITICAL_SECTION g_csA; void main() { HANDLE hThread1,hThread2; //初始化关键代码段 InitializeCriticalSection(&g_csA); //线程的创建 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); //释放关键代码段相关资源 DeleteCriticalSection(&g_csA); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //等待关键代码不被占用时获得关键代码段所有权进入关键代码段 EnterCriticalSection(&g_csA); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放关键代码段所有权 LeaveCriticalSection(&g_csA); } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { //等待关键代码不被占用时获得关键代码段所有权进入关键代码段 EnterCriticalSection(&g_csA); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口2 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放关键代码段所有权 LeaveCriticalSection(&g_csA); } return 0; }
运行结果:
Slim 读/写锁
注意/说明
Slim 读/写锁必须在 Windows Vista 操作系统以及以上版本的操作系统下,且 C++ 编译器版本至少是 VS2008.
SRWLock 允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程).让所有的读取者线程在同一时刻访问共享资源应该是可行的,这是因为仅仅读取资源的值并不存在破坏数据的风险.只有当写入者线程想要对资源进行更新的时候才需要进行同步.这种情况下,写入者线程应该独占对资源的访问权:任何其他线程,无论其他线程,无论是读取者线程还是写入者线程,都不允许访问资源.这就是 SRWLock 提供的全部的功能.我们可以在代码中以一种非常清晰的方式使用它.
首先我们需要分配一个 SRWLOCK 结构并用 InitializeSRWLock 函数对它进行初始化,但是使用后 SRWLOCK 结构体不需要什么特别明确的释放.不像关键代码段那样.
读取/写入
- 写入: 一旦 SRWLock 的初始化完成后,写入者线程就可以调用 AcquireSRWLockExclusive(TryAcquireSRWLockExclusive),将 SRWLOCK 对象地址作为参数传入,以尝试获得对被保护的资源的独占访问权.完成对资源的更新之后,应该调用ReleaseSRWLockExclusive,并将 SRWLOCK 对象的地址作为参数传入,这样就解除了对资源的锁定
- 读取: 对读取者线程来说,同样有两个步骤,但调用的是下面两个函数:先调用 AcquireSRWLockShared(TryAcquireSRWLockShared) ,后调用 ReleaseSRWLockShared.
与关键段相比,SRWLock 缺乏下面特性
- 不能递归地获得 SRWLOCK .也就是说一个线程不能为了多次写入资源而多次锁定资源,然后在多次调用 ReleaseSRWLock* 释放对资源的锁定
同步机制的性能比较
线程\微秒 Volatile读取 Volatile写入 Interlocked递增 关键段 SRWLock共享模式 SRWLock独占模式 互斥量
1 8 8 35 66 66 67 1060
2 8 76 153 268 134 148 11082
3 9 145 361 768 244 307 23785
SRWLock 的性能与关键段性能差不多旗鼓相当.实时上,在许多测试中 SRWLock 的性能要胜过关键段.
我们已经看到,当想让写入者线程和读取者线程以独占模式或共享模式访问同一个资源的时候,可以使用 SRWLock.在这种情况下,如果读取这线程没有数据可以读取,那么它应该将锁是否并等待,直到写入者线程产生了新的数据为止.如果想用来接收写入者线程产生的数据结构已满,那么写入者线程同样应该释放 SRWLock 并进入睡眠状态,直到读取者线程吧数据结构清空为止.
一些有用的窍门和技巧
- 以原子方式操作一组对象时使用一个锁
- 同时访问多个逻辑资源
- 不要长时间占用锁
相关函数
- InitializeSRWLock
- AcquireSRWLockExclusive
- TryAcquireSRWLockExclusive
- ReleaseSRWLockExclusive
- AcquireSRWLockShared
- TryAcquireSRWLockShared
- ReleaseSRWLockShared
执行流程
代码样例(用VS2010调试)
解决火车售票问题
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int tickets=10; SRWLOCK g_srwLock; void main() { HANDLE hThread1,hThread2; //初始化 Slim 读写锁 InitializeSRWLock(&g_srwLock); //创建子线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockExclusive(&g_srwLock); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放 Slim 读写锁 ReleaseSRWLockExclusive(&g_srwLock); } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockExclusive(&g_srwLock); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放 Slim 读写锁 ReleaseSRWLockExclusive(&g_srwLock); } return 0; }
运行结果:
读写锁(读写和写锁的区别)
读锁
程序源码
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int g_x=10; SRWLOCK g_srwLock; void main() { HANDLE hThread1,hThread2; //初始化 Slim 读写锁 InitializeSRWLock(&g_srwLock); //创建子线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { int i = 2; while(i--) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockShared(&g_srwLock); cout<<"线程1获得读锁"<<endl; //数据操作 cout<<"线程1输出g_x变量: "<<g_x<<endl; //cout<<"线程1输出g_x变量: "<<g_x++<<endl; //在读锁中当然也可以对数据进行写入操作,这样数据可能会被写脏. Sleep(10); //释放 Slim 读写锁 ReleaseSRWLockShared(&g_srwLock); cout<<"线程1释放读锁"<<endl; } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { int i = 2; while(i--) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockShared(&g_srwLock); cout<<"线程2获得读锁"<<endl; //数据操作 cout<<"线程2输出g_x变量: "<<g_x<<endl; //cout<<"线程2输出g_x变量: "<<g_x++<<endl; //在读锁中当然也可以对数据进行写入操作,这样数据可能会被写脏. Sleep(10); //释放 Slim 读写锁 ReleaseSRWLockShared(&g_srwLock); cout<<"线程2释放读锁"<<endl; } return 0; }
运行结果
写锁
例程如(上面的解决火车售票问题)
读写锁混用
程序源码
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); DWORD WINAPI Fun3Proc(LPVOID lpParameter); int g_x=10; SRWLOCK g_srwLock; void main() { HANDLE hThread1,hThread2,hThread3; //初始化 Slim 读写锁 InitializeSRWLock(&g_srwLock); //创建子线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); hThread3=CreateThread(NULL,0,Fun3Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(hThread3); Sleep(500); system("pause"); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { int i = 2; while(i--) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockShared(&g_srwLock); cout<<"线程1获得读锁"<<endl; //数据操作 cout<<"线程1输出g_x变量: "<<g_x<<endl; Sleep(10); //释放 Slim 读写锁 ReleaseSRWLockShared(&g_srwLock); cout<<"线程1释放读锁"<<endl; } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { int i = 2; while(i--) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockShared(&g_srwLock); cout<<"线程2获得读锁"<<endl; //数据操作 cout<<"线程2输出g_x变量: "<<g_x<<endl; Sleep(10); //释放 Slim 读写锁 ReleaseSRWLockShared(&g_srwLock); cout<<"线程2释放读锁"<<endl; } return 0; } DWORD WINAPI Fun3Proc(LPVOID lpParameter) { int i = 2; while(i--) { //获得 Slim 读写锁,并独占 Slim 读写锁 AcquireSRWLockExclusive(&g_srwLock); cout<<"线程3获得写锁"<<endl; //数据操作 cout<<"线程3输出g_x变量: "<<g_x++<<endl; Sleep(10); //释放 Slim 读写锁 ReleaseSRWLockExclusive(&g_srwLock); cout<<"线程3释放写锁"<<endl; } return 0; }
运行结果
可以看出读锁不是独占方式占用锁的,而写锁是以独占方式占用锁的.
条件变量
说明/注意
条件变量必须在 Windows Vista 操作系统以及以上版本的操作系统下,且 C++ 编译器版本至少是 VS2008 .
有时候我们想让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止.要实现这样的线程同步是比较复杂的.Windows 通过 SleepConditionVariableCS 或 SleepConditionVariableSRW 函数提供了这一种条件变量
当另一个线程检测到相应的条件已经满足的时候,比如存在一个元素可让读取者线程读取,或者有足够的空间让写入者线程插入新的元素,它会调用 WakeConditionVariable 或 WakeAllConditionVariable,这样阻塞在 Sleep* 函数中的线程就会被唤醒.
当我们调用 WakeConditionVariable 的时候,会使一个在 SleepConditionVariable* 函数中的等待同一个条件变量被触发的线程得到锁并返回.当这个线程释放同一个锁的时候,不会唤醒其他正在等待同一个条件变量的线程.当调用 WakeAllConditionVariable 的时候,会使得一个或几个在 SleepConditionVariable* 函数中等待这个条件变量被触发的线程得到对资源的访问权并返回.唤醒多个线程是可以的,这是因为我们确信如果我们请求独占对资源的访问,那么在同一个时刻必定只有一个写入者线程能得到锁,如果我们传给 Flag 参数是 CONDITION_VARIABLE_LOCKMODE_SHARED,那么在同一时刻可以允许多个读取者线程读到锁.因此,有时候所有的读取这线程会被一起唤醒,有时候会有一个读取者线程先被唤醒,然后是另一个写入者线程,直到所有被阻塞的线程都得到锁为止.
线程同步(内核对象)
互斥对象(mutex)
执行流程
相关函数
代码样例(用VS2005调试)
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int tickets=10; HANDLE hMutex; void main() { HANDLE hThread1,hThread2; //创建互斥对象 hMutex=CreateMutex(NULL,false,"tickets"); //创建子线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); //关闭互斥对象 CloseHandle(hMutex); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //等待互斥对象被释放后获得互斥对象拥有权 WaitForSingleObject(hMutex,INFINITE); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放互斥对象拥有权 ReleaseMutex(hMutex); } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { //等待互斥对象被释放后获得互斥对象拥有权 WaitForSingleObject(hMutex,INFINITE); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //释放互斥对象拥有权 ReleaseMutex(hMutex); } return 0; }
运行结果:
事件对象(Event)
执行流程
解决共享变量问题(人工重置事件不适合在火车售票例子)
相关函数
代码样例(用VS2005调试)
解决共享变量访问问题(用事件对象方法,人工重置事件不适合在个例子中使用,运行结果也会显示出卖出第0张票)
程序源码
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI Fun1Proc(LPVOID lpParameter); DWORD WINAPI Fun2Proc(LPVOID lpParameter); int tickets=10; HANDLE g_hEvent; void main() { HANDLE hThread1,hThread2; //创建人工重置事件 //g_hEvent=CreateEvent(NULL,true,false,NULL); //创建自动重置事件 g_hEvent=CreateEvent(NULL,false,false,NULL); //创建完事件后为无信号,这里设置事件为有信号 SetEvent(g_hEvent); //创建线程 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL); hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL); CloseHandle(hThread1); CloseHandle(hThread2); Sleep(500); system("pause"); CloseHandle(g_hEvent); } DWORD WINAPI Fun1Proc(LPVOID lpParameter) { while(TRUE) { //人工重置事件: 等待该事件为有信号状态 //自动重置事件: 等待该事件为有信号状态,并把事件设置为无信号状态 WaitForSingleObject(g_hEvent,INFINITE); //手动设置信息为有无信号 //ResetEvent(g_hEvent); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口1 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //手动设置事件为有信号 SetEvent(g_hEvent); } return 0; } DWORD WINAPI Fun2Proc(LPVOID lpParameter) { while(TRUE) { //人工重置事件: 等待该事件为有信号状态 //自动重置事件: 等待该事件为有信号状态,并把事件设置为无信号状态 WaitForSingleObject(g_hEvent,INFINITE); //手动设置信息为有无信号 //ResetEvent(g_hEvent); //数据操作 if(tickets>0) { Sleep(10); cout<<"窗口2 售出车票 : 第 "<<tickets--<<" 张票"<<endl; } else break; //手动设置事件为有信号 SetEvent(g_hEvent); } return 0; }
运行结果
线程同步的比较(关键代码段/事件对象/互斥对象)
比较
- 互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步
- 关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值
说明
- 编写多线程程序并需要实现线程同步时,首选关键代码段,由于它的使用比较简单,如果在MFC程序中使用的话,可以再类的构造函数中调用 InitializeCriticalSection 函数,在所需要保护的代码段前调用 EnterCriticalSection 函数,在访问完所需要的资源时候调用 LeaveCriticalSection 函数
- 需要多个进程的各个线程实现同步的话,可以使用互斥对象或事件对象