WIN32汇编语言教程:第12章 多线程 · 12.4 线程间的同步(3)
12.4.2 临界区
了解了同步问题产生的根源,再提出解决方案是很简单的,这在其他的应用程序中早有体现,如各种多用户版的数据库在操作记录之前都要对记录进行锁定,保证一条记录在同一时刻只能被一个对象操作;Windows中的写文件函数在遇到其他程序正在写入中的时候会返回共享错误,而不是不管青红皂白直接写入了事。类似的例子还可以找到很多,归纳起来不外乎一点:就是保证整个存取过程的独占性,在一个线程对某个对象进程操作的过程中,需要有某种机制阻止其他线程的操作。
将这个思路用于多线程之间的同步,可以设计出一些方案来:
(1)设置一个“允许操作”标志变量,当线程需要进行独占操作的时候将标志位复位,操作完成后将标志位置位,任何线程如果需要对对象操作,操作之前必须判断标志位是否置位,如果没有则等待。
(2)如果觉得上面的方法存在CPU占用率的问题,可以使用事件对象来代替自己定义的标志变量。
(3)使用临界区对象(Critical Section Objects)。
考察这些方案,其实方案1和2不一定就能正常工作,因为设置标志和测试标志这个过程是由多条指令完成的,这些指令本身就存在同步问题,比如某个线程测试到标志变量变为“允许”状态,然后它将标志变量的状态复位并开始操作数据,但如果线程在测试标志变量和将标志变量复位之间被打断的话,其他线程可能在这期间也在做同样的事情。将ThreadSynErr例子按照方案1和2修改后运行,就可以发现计数值还是不同步的。
其实Windows提供了专门的解决方案——使用临界区对象。
临界区也是Windows中的一种对象,从理解的角度看,同样可以把它看做是一种标志,只不过多个线程同时操作这个“标志”的时候,由Windows负责标志测试中的同步问题罢了。
临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,结构的具体字段不必关心,也不应该关心,因为它的维护和测试工作都是由Windows来完成的,只需把它想像成一个标志就可以了,结构应当定义成全局变量,因为在各线程中都要测试它。
定义了CRITICAL_SECTION结构后,必须首先对它进行初始化:
invoke InitializeCriticalSection, lpCriticalSection
lpCriticalSection参数指向数据段中定义的CRITICAL_SECTION结构。
假如将需要独占的工作看成是使用一个单人更衣室,那么标志就相当于更衣室门上的牌子,当一个人进入更衣室的时候,将牌子翻到“里面有人”这一面,出来的时候将牌子翻回到“里面无人”这一面,上面的方法1和2就相当于谁先看到这个牌子,谁就可以进入,当几个人同时看到牌子的时候就产生矛盾了。如果使用临界区,就相当于门口站了一个工作人员(这里就是Windows),只有向他申请后获得允许的人才可以进入,其他的人即使同时提交了申请,也将暂时被拦在外面。
所以,定义并初始化临界区以后,当需要对只能独占的数据进行操作的时候,可以先向Windows递交“进入更衣室”的申请,只有里面没有人,Windows才会答复,这个工作由EnterCriticalSection函数来完成:
invoke EnterCriticalSection, lpCriticalSection
如果当前由其他线程拥有临界区,函数不会返回,如果函数返回就表示现在可以独占数据了。调用EnterCriticalSection函数可以看成是让Windows检测标志,如果是“不允许”则等待;是“允许”则将标志修改为“不允许”状态并返回。
当完成操作的时候,还要将临界区交还Windows,以便其他线程可以申请使用,这个工作由LeaveCriticalSection函数完成:
invoke LeaveCriticalSection, lpCriticalSection
LeaveCriticalSection函数的功能可以看成是将标志从“不允许”改回“允许”状态。
当程序不再使用临界区的时候,必须使用DeleteCriticalSection将它删除:
invoke DeleteCriticalSection, lpCriticalSection
现在用临界区来改进前面的ThreadSynErr程序,修改后的代码在所附光盘的Chapter12\ThreadSyn目录中,其中ThreadSyn.rc文件和ThreadSynErr.rc文件没有改动。修改后的ThreadSyn.asm文件如下:
.386 .model flat, stdcall option casemap :none ;#################################################################### ; Include 文件定义 ;#################################################################### include windows.inc include user32.inc includelib user32.lib include kernel32.inc includelib kernel32.lib ;#################################################################### ; Equ 等值定义 ;#################################################################### ICO_MAIN equ 1000 DLG_MAIN equ 1000 IDC_COUNTER1 equ 1001 IDC_COUNTER2 equ 1002 ;#################################################################### ; 数据段 ;#################################################################### .data? hInstance dd ? hWinMain dd ? hWinCount dd ? dwCounter1 dd ? dwCounter2 dd ? dwThreads dd ? stCS CRITICAL_SECTION <?> dwOption dd ? F_STOP equ 0001h .const szStop db '停止计数',0 szStart db '计数',0 ;#################################################################### ; 代码段 ;#################################################################### .code ;#################################################################### _Counter proc uses ebx esi edi,_lParam inc dwThreads invoke SetWindowText,hWinCount,addr szStop and dwOption,not F_STOP .while ! (dwOption & F_STOP) invoke EnterCriticalSection,addr stCS inc dwCounter1 mov eax,dwCounter2 inc eax mov dwCounter2,eax invoke LeaveCriticalSection,addr stCS .endw dec dwThreads invoke SetWindowText,hWinCount,addr szStart ret _Counter endp ;#################################################################### _ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam local @dwThreadID mov eax,wMsg ;******************************************************************** .if eax == WM_TIMER invoke EnterCriticalSection,addr stCS invoke SetDlgItemInt,hWinMain,IDC_COUNTER1,\ dwCounter1,FALSE invoke SetDlgItemInt,hWinMain,IDC_COUNTER2,\ dwCounter2,FALSE invoke LeaveCriticalSection,addr stCS ;******************************************************************** .elseif eax == WM_COMMAND mov eax,wParam .if ax == IDOK .if dwThreads or dwOption,F_STOP invoke KillTimer,hWnd,1 .else mov dwCounter1,0