CPU的同步机制.doc
文本预览下载声明
同步机制漫谈
张银奎 yinkui.zhang@
更快是计算机世界的一个永恒主题。要做到更快有两个方向:一是提高串行执行的速度,二是并行计算(Parallel Computing)。并行计算又可分为同一CPU内部多个流水线间的并行、同一个系统内多个CPU间的并行、和同一个网络中多个计算机系统间的并行。
当并行运行的多个任务彼此无关,互不依赖时,整个系统的性能是最高的。但在现实的并行计算中,这是不可能的。至少同一组内的多个任务之间是存在依赖关系的,它们需要交流信息,报告彼此的计算结果;调整进度,确保各个任务都有条不紊的进行;协调资源,确保共享数据的一致性和安全性和最终结果的正确性。这样便产生了并行计算中的一个基本问题,那就是同步(Synchronization)。并行计算的特征决定了同步是它的一个必然问题。
为了易于理解,我们看一个从银行账户中存款和提款的简单例子,清单1给出的是账户类CAccount的Withdraw和Deposit方法的C++代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 BOOL CAccount::Withdraw(double dblNumber)
{
BOOL bRet=TRUE;
if(GetBalance()=dblNumber)
{
// Send out money now, we use sleep to simulate
Sleep(rand());
m_dblBalance-=dblNumber;
}
else
{
bRet = FALSE;
}
Log(TASK_WITHDRAW,dblNumber,bRet);
return bRet;
}
void CAccount::Deposit(double dblNumber)
{
m_dblBalance+=dblNumber;
Log(TASK_DEPOSIT,dblNumber,TRUE);
} 以上方法很容易理解,参数dblNumber是要支出或存入的金额,第4行检查帐户余额是否足够本次提取,第7行执行支付操作,我们调用Sleep函数延迟一段时间来模拟这个操作,第8行修改账户余额。
图1是使用以上类的TaskSync程序的界面和一次执行记录。点击Deposit和Withdraw按钮会触发创建新的线程来调用CAccount类的Deposit和Withdraw 方法。编辑框中的数字既是存入和支出的金额,又是要创建的线程数,因为我们让每个线程都固定的存入或取出1元钱。
在编辑框中各输入10后,随机的反复点击Deposit和Withdraw按钮,持续一段时间后,我们会发现余额变成了负数。
图1 TaskSync程序
观察清单1中的代码,只有在确保余额不小于参数dblNumber时(第4行)才会执行取款动作,然后递减余额(m_dblBalance)。也就是说这个账户是不应该出现负数余额的(不可透支)。那么,是什么原因导致余额变为负数呢?以下是几种猜想:
在某个(些)线程执行取款动作的过程中(第7行),其它线程又修改了余额值。尽管第4行作判断时账户中还有足够的余额,但是在执行递减操作时,其它线程(提款机)可能已经把余额递减为0了,于是再次递减便出现了负数。
在某个(些)线程更新m_dblBalance变量时,也就是执行递减操作(第8行)时,其它线程又修改了它的值。清单2列出了m_dblBalance-=dblNumber语句所对应的汇编代码。可见尽管C++是一条语句,但是编译出的汇编语句还是有很多条的。第1行(清单2)是将this指针存入EAX寄存器,第2行是将m_dblBalance(this+8)从内存加载到FPU(符点处理单元)寄存器栈中,第3行是执行减法运算,ebp+8指向的是参数dblNumber,第4行是将this指针存入ECX寄存器,第5行是将计算结果存回内存中的m_dblBalance成员变量。因为第3行的减法计算是对加载在CPU寄存器中的值做减法,第5行再将这个值存回内存,那么如果在某个线程执行2、3条指令的间隙,其它线程修改了m_dblBalance,那么这个线程使用的仍然是旧的数据,而且第5行会将错误的结果写入到内存中。
在32位x86系统中,m_dblBalance变量在内存中的长度是8个字节(QWORD)。这意味着存取这个变量时需要读写8个字节。如果,两个线程恰好都要读写这8个字节,那么有可能某个线程读到的内容是另一个线程写了一半的数据,或者某个线程写了8个字节的前半部分,另一个线程写了后半部分。
清单2 m_dblBalance-=dblNumber语句所对应的汇编代码
1
2
3
4
5 00
显示全部