我经常在快餐厅吃饭,可以自己加饭的那种,店里有好些服务器专门负责收掉已就餐完毕的客人的餐具。但是由于客人离开座位去加饭,导致服务员误收餐具的情况。所以我每次去加饭前,都会把自己的眼镜放在菜碟旁边,表示我还需要继续用餐,你不要收走我的餐具_。
[TOC]
锁与同步机制
其实上面的例子就是用来说明计算机程序中锁是什么东东的一个很好的例子。模块A和模块B在某一个时刻都需要操作数据C,那么他们必须要以一种正确的方式来操作C,才能保证C不会出问题;这里的模块A相当于餐厅就餐中的客人,而模块B可以看作服务员,数据C就相当于要收拾的餐具。要保证收盘子不会出问题,我们需要一种机制——同步机制D,放置眼镜这个事实,来使得客人和服务员正确的使用盘子,也就是正确的操作数据C[1]。
同步机制是计算机编程里一个概念。我们说的“锁”,可以是这个同步机制的别称,也可以是实现同步机制的实体名称,比如读写锁和文件锁。除了这些带”锁“结尾的实体,还有比如互斥量,信号,信号量,描述符IO都可以实现这种机制。这里的概念有些混乱,我表达得也不是很好,请大家多体会一下实体和概念的定义,这里“锁 ”的两个定义,就像价值和价格一样。
锁的本质
锁是同步机制的实现,其本质是状态机,只有状态机在正确的状态上才能操作数据,否则就等待。下面用我们要实现的原子锁模拟上锁与解锁的过程。模块A与模块B同时操作数据C,并使用原子锁D进行同步,设原子锁的状态机未锁定状态为0,已锁定状态为1,且初始状态为0,假设时序为模块A先通过操作锁发起操作数据C的请求。整个操作过程的时序用以下的表格表示[2]。
时序 | 模块A操作 | 模块B操作 | 原子锁D状态 | 数据C所属 |
---|---|---|---|---|
开始 |
nil |
nil |
0 |
nil |
A优先B改变D的状态 |
while(1){ /*it will run this code and then break loop*/ if(D.stat==0) D.stat = 1; if (D.stat==1) break; }
|
do other something |
turn stat from 0 to 1 |
Critical state |
A操作C,B等待A再次复位D的状态 |
do something to data C |
while(1){ /*it will always in loop until D's stat turn from 1 to 0*/ if(D.stat==0) D.stat = 1; if (D.stat==1) break; }
|
1 |
model A |
A复位D的状态,B将要退出循环 |
D.stat = 0 |
while(1){/*it will break loop*/} |
turn stat from 1 to 0 |
Critical state |
B改变D的状态 |
do other something |
if(D.stat==0) D.stat = 1 |
turn stat from 0 to 1 |
Critical state |
B操作 |
do other something |
do something to data C |
1 |
model B |
B复位D的状态 |
do other something |
D.stat = 0 |
turn stat from 1 to 0 |
Critical state |
结束 |
nil |
nil |
0 |
nil |
模块AB为了安全的操作数据C都会先改变原子锁D的状态,如果D的状态不符合要求就等待直到条件允许,在成功改变D的状态后,才操作数据C,操作完毕后复位D的状态到改变前,至此流程结束。模块AB共同准守通过锁D对数据C进行操作的流程规范就是一种同步机制,锁是实现这个机制的手段(媒介)。任何模块在操作数据时,只有大家都准守这个同步机制,数据才不会发生混乱,否则数据就会出现一致性错误。比如还是以餐厅就餐的问题做比喻,如果我用眼镜提示服务员不要收走我的餐具,但是他对这个嗤之以鼻,他还是可以收走我的餐具,我对此却毫无办法。
为何会发生同步问题
其主要原因就是需要“同时”操作数据。从就餐问题可以看出,如果服务员收盘子和我去加饭不是同时发生,就不会出现误收餐具的事情,当然不排除服务员因为不满意老板发的薪水,故意当客人的面就把餐具没收的情况。所以如果不是多线程程序,绝大多数都不需要同步机制的,但不排除信号中断也会在单进程产生同步问题,还有协程机制也会,虽然他不是绝对定义上的同时操作,后面的文章会一一举例这些情况。
临界点
当操作者开始改变数据的那一个时间点,就是临界点。如果有多个操作者在这个点操作数据,那么这就可能会发生数据一致性错误,那么这个零界点就是需要保护的临界点。在餐厅问题中,我离开打饭,而服务员要收走餐具,他做出这个动作时就产生了零界点,如果他只是过来看一看,这个动作不算,因为他没有对我的就餐产生影响。
临界区
任何可能产生临界点的区域就是临界区,锁需要保护的是整个临界区。因为服务员是在餐厅来回走动,如果他发现某位置上已是杯盘狼藉,没有客人且也没有任何提示,所以从客人离开桌子和服务员的视线直到回到座位上之前,他都有可能会来收取餐具,所以我在离开时,放置眼镜以作提示,直到我再次回到座位上。这段时间就是一个临界区,我们需要上锁保护的是这段时间,而非一点。
总结
- 同步机制是为了保护数据的一致性。
- 通常所说的“加锁”其实指的是采用同步机制,该处的锁是概念上的。
- 通常所说的锁是一个数据结构上的实体,也是实现同步机制的一种手段,但非唯一手段。
- 因为会临界点和临界区所以才会需要同步机制,如果多个操作者不会修改数据,那么就不要同步。
-
这里的就餐问题和同步机制在概念上是说得通的,但不能等同锁的使用。 ↩
-
这里的表格只是仅仅为简单的说明时序,并没有表达原子操作和真正的实现。
[原子操作]://www.greatytc.com/p/cb7b726e943c ↩