2019年07月17日(星期三)  农历:己亥年六月十五

作者:三年。分类: Linux

为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机 制保证进程之间不会自说自话而是有效的协同工作。比如在共享内存的通信方式中,两个或者多个进程都要对共享的内存进行数据写入,那么怎么才能保证一个进程 在写入的过程中不被其它的进程打断,保证数据的完整性呢?又怎么保证读取进程在读取数据的过程中数据不会变动,保证读取出的数据是完整有效的呢?

常用的同步方式有: 互斥锁、条件变量、读写锁、记录锁(文件锁)和信号灯

1、互斥锁

顾名思义,锁是用来锁住某种东西的,锁住之后只有有钥匙的人才能 对锁住的东西拥有控制权(把锁砸了,把东西偷走的小偷不在我们的讨论范围了)。所谓互斥,从字面上理解就是互相排斥。因此互斥锁从字面上理解就是一点进程 拥有了这个锁,它将排斥其它所有的进程访问被锁住的东西,其它的进程如果需要锁就只能等待,等待拥有锁的进程把锁打开后才能继续运行。

在实现中,锁并不是与某个具体的变量进行关联,它本身是一个独立的对象。进(线)程在有需要的时候获得此对象,用完不需要时就释放掉。

互斥锁的主要特点是互斥锁的释放必须由上锁的进(线)程释放,如果拥有锁的进(线)程不释放,那么其它的进(线)程永远也没有机会获得所需要的互斥锁。互斥锁主要用于线程之间的同步。

2、条件变量

上文中提到,对于互斥锁而言,如果拥有锁的进(线)程不释放锁,其它进(线)程永远没机会获得锁,也就永远没有机会继续执行后续的逻辑。在实际 环境下,一个线程A需要改变一个共享变量X的值,为了保证在修改的过程中X不会被其它的线程修改,线程A必须首先获得对X的锁。现在假如A已经获得锁了, 由于业务逻辑的需要,只有当X的值小于0时,线程A才能执行后续的逻辑,于是线程A必须把互斥锁释放掉,然后继续"忙等".如下面的伪代码所示:

// get x lock

while(x <= 0){

// unlock x ;

// wait some time

// get x lock

}

// unlock x

这种方式是比较消耗系统的资源的,因为进程必须不停的主动获得锁、检查X条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候 才可以。因此我们需要另外一种不同的同步方式,当线程X发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线 程。其它线程此时就有机会去修改X的值,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的 计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。

坦率的说,从字面意思上来将,"条件变量"这四个字是不太容易理解的。我们可以把"条件变量"看做是一个对象,一个铃铛,一个会响的铃铛。当一 个线程在获得互斥锁之后,由于被锁定的变量不满足继续运行的条件时,该线程就释放互斥锁并把自己挂到这个"铃铛"上。其它的线程在修改完变量后,它就摇 摇"铃铛",告诉那些挂着的线程:"你们等待的东西已经变化了,都醒醒看看现在的它是否满足你们的要求。"于是那些挂着的线程就知道自己醒来看自己是否能 继续跑下去了。

3、读写锁

互斥锁是排他性锁,条件变量出现后和互斥锁配合工作能够有效的节省系统资源并提高线程之间的协同工作效率。互斥锁的目的是为了独占,条件变量的 目的是为了等待和通知。但是现实世界是很复杂di,我们要解决的问题也是多种多样di.从功能上来说,互斥锁和条件变量能够解决基本上所有的问题,但是性 能上就不一定完全满足了。人的无休止的欲望促使人发明出针对性更强、性能更好的同步机制来。读写锁就是这么一个玩意儿。

考虑一个文件有多个进程要读取其中的内容,但只有1个进程有写的需求。我们知道读文件的内容不会改变文件的内容,这样即使多个进程同时读相同的 文件也没什么问题,大家都能和谐共存。当写进程需要写数据时,为了保证数据的一致性,所有读的进程就都不能读数据了,否则很可能出现读出去的数据一半是旧 的,一半是新的状况,逻辑就乱掉了。

为了防止读数据的时候被写入新的数据,读进程必须对文件加上锁。现在假如我们有2个进程都同时读,如果我们使用上面的互斥锁和条件变量,当其中 一个进程在读取数据的时候,另一个进程只能等待,因为它得不到锁。从性能上考虑,等待进程所花费的时间是完全的浪费,因为这个进程完全可以读文件内容而不 会影响第一个,但是这个进程没有锁,所以它什么也做不了,只能等,等到花儿都谢了。

所以呢,我们需要一种其它类型的同步方式来满足上面的需求,这就是读写锁。

读写锁的出现能够有效的解决多进程并行读的问题。每一个需要读取的进程都申请读锁,这样大家互不干扰。当有进程需要写如数据时,首先申请写锁。 如果在申请时发现有读(或者写)锁存在,则该写进程必须等待,一直等到所有的读(写)锁完全释放为止。读进程在读取之前首先申请读锁,如果所读数据被写锁 锁定,则该读进程也必须等待读锁被释放位置。

很自然的,多个读锁是可以共存的,但写锁是完全互相排斥的。

4、记录锁(文件锁)

为了增加并行性,我们可以在读写锁的基础上进一步细分被锁对象的粒度。比如一个文件中,读进程可能需要读取该文件的前1k个字节,写进程需要写 该文件的最后1k个字节。我们可以对前1k个字节上读锁,对最后1k个自己上写锁,这样两个进程就可并发工作了。记录锁中的所谓"记录"其实是"内容"的 概念。使用读写锁可以锁定一部分,而不是整个文件。

文件锁可以认为是记录锁的一个特例,当使用记录锁锁定文件的所有内容时,此时的记录锁就可以称为文件锁了。

5、信号灯

信号灯可以说是条件变量的升级版。条件变量相当于铃铛,铃铛响后每个挂起的进程还需要自己获得互斥锁并判断所需条件是否满足,信号灯把这两步操作糅合到一起。

在Posix.1基本原理一文声称,有了互斥锁和条件变量还提供信号灯的原因是:"本标准提供信号灯的而主要目的是提供一种进程间同步的方式; 这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的;这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同 步方式。每组原语都特别适合于特定的问题".尽管信号灯的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号灯也可用于线程间,互斥锁 和条件变量也可用于进程见。应当根据实际的情况进行决定。

信号灯最有用的场景是用以指明可用资源的数量。比如含有10个元素的数组,我们可以创建一个信号灯,初始值为0.每当有进程需要读数组中元素时 (假设每次仅能读取1个元素),就申请使用该信号灯(信号灯的值减1),当有进程需要写元素时,就申请挂出该信号等(信号灯值加1)。这样信号灯起到了可 用资源数量的作用。如果我们限定信号灯的值只能取0和1,就和互斥锁的含义很相同了。

温馨提示如有转载或引用以上内容之必要,敬请将本文链接作为出处标注,谢谢合作!

已有 0/1832 人参与

发表评论:



手Q扫描加入Java初学者群