最近在做《0bug-C/C++商用工程之道》的勘误表,重新审查了本书。

发现P41页,2.3小节关于锁的论述有点含混不清,决定予以修订。

主要问题出在我是用32bits的数字在举例子,说明会分两个16bits先后存放,这在16bits的机器上是正确的,但是在目前32bits系统大行其道的今天,其实已经不准确了,因此,重新修订,使用64bits的数字来举例子,基本就能说清楚问题了。

前文已经说明,在商用数据传输中,面临的是一个并行的开发环境,同步和异步处理是软件研发的技术核心。而对于多任务操作系统而言,异步转同步最常用的工具,就是“锁”。

“锁”有很多形式,如临界区,信号量,等等。本书为了理解方便,统一称为“锁”。

2.3.1  为什么要使用锁

锁的由来,起源于CPU对内存的操作。我们知道,不管CPU如何进步,16位到32位,再到现在的64位,其实主要说的是两点,一个是int或者long这个变量类型,即整型数,代表的位数,这表明了计算机一次运算中能处理的最大数字。一个是CPU的地址寄存器,由于位数的增加,使可以连续编址,同时访问到的内存空间在大幅度扩大。

但不管怎么变化,我们发现,计算机的内存,其实还是以字节(Byte)在计数,即8位二进制,是一个内存单元。CPU每个时钟周期,至少能通过总线访问到1个字节。

我们以32CPU下的32位操作系统举例,比如说,我们需要对一个8字节的64 long型数字做加法计算,CPU会一次从内存先读入4个字节的数据,这是CPU一次能处理的极限位宽,然后做加法运算,然后将数字存回内存,下一周期,CPU会再次读入另外4个字节的数据,做进位累加等计算。

但这就带来一个问题,由于是多任务环境并行运算,这个加法计算可能会被打断,比如一个计算,我们把0x 0000 0000 FFFF FFFF做加1的计算,结果应该是0x 0000 0001 0000 0000

但实际运行时,当CPU仅仅做了低4字节的加法计算,写回内存后,尚未来得及做第二步计算,将进位累加到高4字节时,如果此时发生操作系统的任务切换,有其他线程切进来读取这个数字,这个数字就变成了旧的高32+新的低32位, 0x 0000 0000 0000 0000,这显然是个错误的数字。

此时,很不幸,正好切进来读取该数字的线程,读到了一个明显非法的结果,而依赖这个数据的后续计算,就都不对了,严重的,如果这个单元是一个指针的话,就可能导致程序直接崩溃。如下图。

 

 

2.7:内存跨线程争用错误演示


 

    这种错误非常可怕,通常都是“写”的线程“犯错误”,而“读”的线程崩溃,bug点不是制造事故的第一责任人,因此很难从错误点来跟踪源头,这类错误,一般都应该从程序设计的一开始就小心谨慎,尽量避免。

多任务操作系统针对这个问题,提出的解决方案就是加锁。加锁事实上是一种宣告,告知操作系统和CPU,本线程的下面几个动作不能被打断,直到解锁为止。

因此,当两个线程都按照“加锁-访问-解锁”这个顺序做事时,如果第一个线程加锁后,第二个线程试图访问加锁的资源,则被系统悬挂等待,直到锁解除后,才可以继续执行,这样就能确保读到正确的数据。如图2.8

 

 

2.8:使用锁规避内存争用bug演示


 

    由此可见,使用锁,是不同线程访问相同资源的必要条件,否则程序的运行结果将不可知。

在商用工程领域,这个“锁”的概念还可以进一步抽象,经常是我们处理一笔复杂业务,需要连续几个动作不能打断,这时候,也可以使用锁这个概念。

但这个时候,锁的形式就多种多样了,可以是一个上面提到的操作系统原子锁,也可以是一个字节的内存单元实现(1字节的操作不会被打断),也可以是一个数据库的锁定动作,甚至,可以专门设计一台锁服务器,协调服务器集群中,各个业务服务器之间的同步协调。

 

===================================================
肖舸 《0bug-C/C++商用工程之道》
QQ:712123
MSN/Email:tonyxiaohome@hotmail.com
个人主页:http://g.51cto.com/tonyxiaohome