📄 012.txt
字号:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Counter proc uses ebx esi edi,_lParam
inc dwThreads
invoke SetWindowText,hWinCount,addr szStop
and dwOption,not F_STOP
.while ! (dwOption & F_STOP)
inc dwCounter1
mov eax,dwCounter2
inc eax
mov dwCounter2,eax
.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 SetDlgItemInt,hWinMain,IDC_COUNTER1,\
dwCounter1,FALSE
invoke SetDlgItemInt,hWinMain,IDC_COUNTER2,\
dwCounter2,FALSE
;********************************************************************
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDOK
.if dwThreads
or dwOption,F_STOP
invoke KillTimer,hWnd,1
.else
mov dwCounter1,0
mov dwCounter2,0
xor ebx,ebx
.while ebx < 10
invoke CreateThread,NULL,0,\
offset _Counter,NULL,\
NULL,addr @dwThreadID
invoke CloseHandle,eax
inc ebx
.endw
invoke SetTimer,hWnd,1,500,NULL
.endif
.endif
;********************************************************************
.elseif eax == WM_CLOSE
.if ! dwThreads
invoke EndDialog,hWnd,NULL
.endif
;********************************************************************
.elseif eax == WM_INITDIALOG
push hWnd
pop hWinMain
invoke GetDlgItem,hWnd,IDOK
mov hWinCount,eax
;********************************************************************
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,eax,DLG_MAIN,NULL,\
offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
目录中的ThreadSynErr.rc文件定义了如图12.3所示的界面。
图12.3 多线程同步的演示程序
ThreadSynErr.rc文件的代码为:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_COUNTER1 1001
#define IDC_COUNTER2 1002
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 227, 187, 129, 56
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "多线程同步的演示程序"
FONT 9, "宋体"
{
LTEXT "计数器一:", -1, 7, 7, 40, 8
LTEXT "计数器二:", -1, 7, 22, 41, 8
EDITTEXT IDC_COUNTER1, 51, 5, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP
EDITTEXT IDC_COUNTER2, 51, 20, 71, 12, ES_READONLY | WS_BORDER | WS_TABSTOP
PUSHBUTTON "计数", IDOK, 72, 36, 50, 14
}
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
“问题程序”还是用循环计数的功能来演示,程序中设置了两个全局变量dwCounter1和dwCounter2用做计数器,当按下“计数”按钮的时候,程序将两个计数器清零,并且用循环语句建立10个线程来同时执行_Counter线程函数:
mov dwCounter1,0
mov dwCounter2,0
xor ebx,ebx
.while ebx < 10
invoke CreateThread,NULL,0,offset _Counter,NULL,\
NULL,addr @dwThreadID
invoke CloseHandle,eax
inc ebx
.endw
invoke SetTimer,hWnd,1,500,NULL
最后,程序设立一个定时器来定时将计数器值显示到编辑框中。
在线程函数中,使用下面的计算代码:
.while ! (dwOption & F_STOP)
inc dwCounter1
mov eax,dwCounter2
inc eax
mov dwCounter2,eax
.endw
在这段代码中,递增第一个计数器使用inc指令,由于计算用单条指令完成,所以计数器一不会因为同步问题出错,递增第二个计数器的代码使用了3条指令,首先将原来的计数值取到eax中,递增eax后再写回到变量中,如果不存在多个线程同步的问题,这两种算法的结果是一样的,显示到编辑框中的计数值应该是相等的。
在存在同步问题的情况下,如果线程在mov eax,dwCounter2或者inc eax指令执行后被打断,并且其他线程在这期间修改了dwCounter2的值的话,根据前面的分析,就会有一次计数值被丢失。如果显示到编辑框中的计数值不相等,则证明存在同步的问题,通过比较两个计数值的差值,还可以得知同步问题发生的机会是多少。
好了,大家可能都迫不及待地想看运行结果了吧,结果如图12.3所示,这是程序在笔者的450 MHz的计算机上运行了10秒以后的结果,可以看到,10个线程加起来总共进行了 783 189 430次计算,计数器二却丢失了783 189 430-274 090 739=509 098 691次计数,因为同步问题丢失的计数竟然占了65%,可见这绝对不是偶尔发生一次两次的事情,大家可以想像一下,如果有人往一个银行账户中汇款,三笔汇款中丢了两笔,人们会有何感想呢?
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递交“进入更衣
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -