什么是线程同步
线程同步是指多个线程之间的协调同步,按照一定的次序进行执行。Linux中的线程同步机制主要有互斥锁、自旋锁、读写锁和条件变量四种。互斥锁与自旋锁在使用形式上比较类似,都是前一个线程在加锁后会阻止后来想要加锁线程被阻塞或者返回错误。我们可以把读写锁比作前两者的延伸,这个机制允许不同线程在同一读写锁上添加读锁,不允许在同一读写锁上添加写锁,而且读写锁相互排斥,有了读锁的读写锁不允许再添加写锁,有了写锁的读写锁也不允许再添加读锁。在条件变量中,被条件变量阻塞的线程需要另一线程释放信号来唤醒。
为什么需要线程同步
数据竞争:当多个线程对同一共享数据进行读写操作时,由于并发执行的随机性,会导致它们的操作出现冲突,从而导致数据的混乱与不一致。
死锁:当多个线程同时竞争多个共享资源时,由于资源的有限性以及线程不合理的发生顺序,会导致线程的相互等待,产生死锁情况。
饥饿:当某些线程始终无法获得对共享资源的访问权时,就可能导致饥饿的情况,这些线程会一直处于等待状态,无法执行下去。
如何选择同步机制
阻塞等待状态,而自旋锁会一直循环检查所是否可用,而并不会让线程进入阻塞状态。在整个运行过程中,使用互斥锁会在用户态与内核态中切换,而自旋锁只在用户态中运行。
在高并发的场景下,当多个线程在竞争同一个锁,如果使用自旋锁,自旋锁会不断询问锁的执行情况,占用大量的CPU资源。相对而言,如果使用互斥锁,当一个线程获取锁失败时会进入阻塞状态,放弃CPU的运行,直到该锁可用并被唤醒后再继续执行。因此,在高并发场景中,并且锁的竞争比较激烈的时候,使用互斥锁比使用自旋锁更有效,因为这样可以节省CPU资源,提高系统的并发性能。
以上三种锁,虽然在实现与使用上有些差异,但是仍然可以把它们看做一个爹妈生的,不过条件变量就不同了。如果站在锁的角度来衡量以上四种方法,在前三者方法通过用锁制约其它线程,而自己也被锁锁限制,对于条件变量的信号发送线程来说,这波它站在大气层,它就像远程的管理着锁的开闭,自己却不会受到这把“锁”的直接影响。以前觉得前三者在编程中只要不在乎效率,完全可以相互替代,那条件变量存在的意义是什么呢,思考后我发现前三者中线程的关系是平等的,它们公平的去竞争;而条件变量中信号的发起线程像是一个主宰,决定着其它线程的运行状态。
竞争就好了?为什么要专政呢?
以上的讨论在大方向上给出了线程同步的用法,在使用的细节上也给大家提出些许建议。应该减少不必要的加锁时间,我们在使用锁的过程中主要有初始化、加锁、解锁和释放锁几个步骤,在代码段中的加锁与解锁的位置会直接影响该线程拥有锁的时间,从而对整体代码的效率造成影响,我们要避免将无关的代码放入代码块中,从而缩短锁的范围和时间。
锁要留给谁
类比众多调度算法所考虑的那样,都是在效率与公平间取得平衡,锁的继承也不例外。对锁而言,公平性是指所有线程都有机会获得锁,而不是让某些锁永远没有得到锁的机会;效率是指尽可能减少线程的等待时间。
比较常见的调度算法是先进先出调度,即按照线程等待锁的先后顺序,依次将锁分配给等待时间最久的线程,另一种算法是将锁分配给还没有进入阻塞状态的线程。前者可以保证线程使用锁的公平性,后者则是通过减少阻塞与就绪态的切换来提高系统效率。