WIN32汇编语言教程:第12章 多线程 · 12.4 线程间的同步(1)
12.4.1 产生同步问题的原因
对于多线程的程序来说,线程之间的同步永远是个重要的问题。如果多个线程都要存取同样的对象(如存取同样的内存变量或读写同一个文件等),而一个线程操作的结果反过来又会影响另一个线程的运行的时候,同步问题就变得异常重要。
产生同步问题的根源在于线程之间的切换是无法预测的,一个线程无法知道什么时候自己的时间片会结束,也无法知道下一个时间片会被分配给哪个线程。事实上,线程可以在任何地方被Windows打断。读者应该记住的是:惟一可以确定的事实就是线程只能在两条指令之间被打断,因为指令是CPU执行的最小单位,线程不可能在一条指令执行到一半的时候被打断。
对于单线程的程序来说,主线程在单个时间片结束的时候被Windows挂起,然后在轮到下一个时间片的时候继续执行,这中间整个进程的环境不会有任何改变,因为进程中不存在其他线程。但对于多线程的程序来说,在主线程挂起的过程中可能会有子线程被分配了时间片,如果子线程在执行中改变了主线程正在存取的对象,就可能会引发错误的结果。举一个例子来说明,假定往银行账户里存钱的操作步骤有3步:
(1)获取账户的余额。
(2)将用户存入的数额和余额相加,得到新的余额。
(3)将账户中的余额数据更新为新的数值。
现在从两个不同的储蓄所里同时向一个账户存钱,假设原来的余额是10 000元,储蓄所A要存入1 000元,储蓄所B要存入2 000元,如果没有同步机制,就可能发生下面的情况:
① 蓄所A首先执行第(1)步,获得余额数据10 000,然后进行第(2)步运算得到结果11 000元。
② 这时储蓄所B的业务也刚好发生,在储蓄所A计算第(2)步的过程中,储蓄所B执行了第(1)步,由于储蓄所A还没有执行到第(3)步,所以账户余额还没有被更新,储蓄所B得到的余额数据还是10 000元。
③ 储蓄所B计算新余额,得到结果12 000元。
④ 在储蓄所B计算新余额的过程中,储蓄所A执行了第(3)步,将余额更新为11 000元。
⑤ 最后,储蓄所B执行了第(3)步,将自己的计算结果12 000元更新到余额数据中。
结果就是储蓄所A的业务实际上是丢失了;另一种情况,假如储蓄所B的动作很快,在上面的第④步骤发生之前,在第③步骤中就将计算结果12 000更新到余额数据中了,那么在第④步骤中储蓄所A的计算结果11 000就会将12 000覆盖,这时的结果就是储蓄所B的业务丢失了,所以同步问题产生的错误结果是很难预测的。
将这个比喻引伸到两个线程的同步问题上就表现在:假如线程A将某个内存变量的值取到eax寄存器中,准备在经过运算后将结果写回去,这时被Windows切换到线程B中,线程B在这个时间片中对同一个内存变量进行了修改,当切换回线程A的时候,线程A在上一个时间片中刚取到eax中的数值和内存变量中的值就不同步了,计算结果当然就是错误的。
有人可能会认为出现这种情况的概率是很低的,线程中有这么多条指令要执行,难道偏偏就在程序取完数据还没开始处理的时候被系统打断吗?通过下面的例子就可以发现发生这种事情的可能性有多大。例子程序位于所附光盘的Chapter12\ThreadSynErr目录中,还是用递增计数器的方法来演示同步问题,ThreadSynErr.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 ? dwThreads dd ? dwOption dd ? F_STOP equ 0001h dwCounter1 dd ? dwCounter2 dd ? .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) 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
上页:第12章 多线程 · 12.3 使用事件对象控制线程(2) 下页:第12章 多线程 · 12.4 线程间的同步(2)