刚接触线程的时候懵懵懂懂,懵懵逼逼,只是依稀记得线程需要同步,至于这么做的原因好像是避免线程由于对数据的竞争导致不可预知的结果。随着头发日渐稀疏,对线程同步的理解也不断加深了。

什么是线程同步


线程同步是指多个线程之间的协调同步,按照一定的次序进行执行。Linux中的线程同步机制主要有互斥锁、自旋锁、读写锁和条件变量四种。互斥锁与自旋锁在使用形式上比较类似,都是前一个线程在加锁后会阻止后来想要加锁线程被阻塞或者返回错误。我们可以把读写锁比作前两者的延伸,这个机制允许不同线程在同一读写锁上添加读锁,不允许在同一读写锁上添加写锁,而且读写锁相互排斥,有了读锁的读写锁不允许再添加写锁,有了写锁的读写锁也不允许再添加读锁。在条件变量中,被条件变量阻塞的线程需要另一线程释放信号来唤醒。

为什么需要线程同步


当一个线程被创建后,我们就失去了干涉该线程运行的权利。如果不加干涉地让线程自由运行,可能会在某些情境下引发很多问题。以下是由于线程不同步而常见的问题:

数据竞争:当多个线程对同一共享数据进行读写操作时,由于并发执行的随机性,会导致它们的操作出现冲突,从而导致数据的混乱与不一致。

死锁:当多个线程同时竞争多个共享资源时,由于资源的有限性以及线程不合理的发生顺序,会导致线程的相互等待,产生死锁情况。

饥饿:当某些线程始终无法获得对共享资源的访问权时,就可能导致饥饿的情况,这些线程会一直处于等待状态,无法执行下去。。

如何选择同步机制


互斥锁与自旋锁在处理的场景上大致相同,不过由于其实现方式而引发了一些差异。互斥锁无法获得锁时,会进入阻塞等待状态,而自旋锁会一直循环检查所是否可用,而并不会让线程进入阻塞状态。在整个运行过程中,使用互斥锁会在用户态与内核态中切换,而自旋锁只在用户态中运行。

以上的差异导致了这两种锁在应用场景的差异。互斥锁适合等待比较长的场景,因为线程在等待锁的过程中会进入阻塞状态,不会消耗CPU资源;而自旋锁适用于县城的等待时间比较短的情景,因为县城在等待锁的过程会一直检查锁是否可用,会消耗CPU资源。

在高并发的场景下,当多个线程在竞争同一个锁,如果使用自旋锁,自旋锁会不断询问锁的执行情况,占用大量的CPU资源。相对而言,如果使用互斥锁,当一个线程获取锁失败时会进入阻塞状态,放弃CPU的运行,直到该锁可用并被唤醒后再继续执行。因此,在高并发场景中,并且锁的竞争比较激烈的时候,使用互斥锁比使用自旋锁更有效,因为这样可以节省CPU资源,提高系统的并发性能。

读写锁其实可以看做互斥锁的一种特殊情景,我们对于数据或资源的使用无外乎读取与修改,但是当一群线程对该资源或数据的使用仅限于正确的读取,那么使用互斥锁就有些大动干戈了,我们只需要保证该数据或资源的一致性,而不必要求其它读取线程的等待锁的释放。所以说,使用读写锁可以提高系统在处理读取任务时的效率。

以上三种锁,虽然在实现与使用上有些差异,但是仍然可以把它们看做一个爹妈生的,不过条件变量就不同了。如果站在锁的角度来衡量以上四种方法,在前三者方法通过用锁制约其它线程,而自己也被锁锁限制,对于条件变量的信号发送线程来说,这波它站在大气层,它就像远程的管理着锁的开闭,自己却不会受到这把“锁”的直接影响。以前觉得前三者在编程中只要不在乎效率,完全可以相互替代,那条件变量存在的意义是什么呢,思考后我发现前三者中线程的关系是平等的,它们公平的去竞争;而条件变量中信号的发起线程像是一个主宰,决定着其它线程的运行状态。

竞争就好了?为什么要专政呢?

用现实生活中的例子来看,如果一群人相互竞争,谁也不服谁,那他们注定不能变的井井有条,如果是单独的任务还好,一旦遇上需要合作的情况,这时候就需要一个独裁者来统筹规划,来指明方向。再来用编程的角度来看,通过互斥的竞争手段可以保护共享资源,使得同一时间只有一个线程共享访问资源,而条件变量可以用来协调线程的行为,它让一个线程等待另一个线程的通知,从而协调线程执行的顺序和进度。

以上的讨论在大方向上给出了线程同步的用法,在使用的细节上也给大家提出些许建议。应该减少不必要的加锁时间,我们在使用锁的过程中主要有初始化、加锁、解锁和释放锁几个步骤,在代码段中的加锁与解锁的位置会直接影响该线程拥有锁的时间,从而对整体代码的效率造成影响,我们要避免将无关的代码放入代码块中,从而缩短锁的范围和时间。

锁要留给谁


我们通过编程可以决定锁由谁加,不过当一个锁被释放时正好有多个线程在等待锁,接下来锁会分配给谁则是操作系统的调度算法来实现的。

类比众多调度算法所考虑的那样,都是在效率与公平间取得平衡,锁的继承也不例外。对锁而言,公平性是指所有线程都有机会获得锁,而不是让某些锁永远没有得到锁的机会;效率是指尽可能减少线程的等待时间。

比较常见的调度算法是先进先出调度,即按照线程等待锁的先后顺序,依次将锁分配给等待时间最久的线程,另一种算法是将锁分配给还没有进入阻塞状态的线程。前者可以保证线程使用锁的公平性,后者则是通过减少阻塞与就绪态的切换来提高系统效率。