介绍
线程是操作系统中任务调度器可以调度的最小单元。它类似于进程,但区别于多个线程可以共享同一地址空间。在多线程的进程中,地址空间有多个栈。并发的关键术语:
-
临界区(critical section):访问共享资源的一段代码。
-
竞态条件(race condition):多个线程几乎同时进入临界区更新共享数据结构。
-
不确定性(Indeterminate):程序由一个或多个竞态条件组成,程序输出因运行而异。
-
互斥(mutex):用于保护共享变量被多个线程同时更新的对象。
-
原子(atomic):事务要么发生要不一个单个原子操作都不执行。
锁
锁是一个保护共享数据被多个线程同时改变的控制原语。通过使用互斥锁来保护临界区,避免竞态条件,使得事务的执行是原子的。评价锁的标准包括正确性、公平性和性能。
实现互斥锁的策略包括使用TestAndSet或者CompareAndSet原子指令实现的自旋锁和使用队列实现的休眠锁策略。Linux使用的mutex策略是两阶段锁,即先自旋(fastpath)后休眠(slowpath),其中,fastpath的引入可以避免只使用运行-休眠策略带来的频繁切换线程上下文产生的寄存器压力,slowpath的引入可以避免只是用自旋带来的CPU时钟的潜在浪费。
读写锁的实现策略核心是,让第一个获得读锁的读者获取写锁,让最后一个读者释放写锁。通过记录等待写者,可以避免写者饿死。与简单的互斥锁相比,读写锁有更多的CPU开销。
条件变量
条件变量是一个显示队列,当条件不满足时,线程把自己加入等待队列,等待该条件。当条件被改变时,线程唤醒队列中的等待线程。常用于解决的问题包括生产者/消费者问题(producer/consumer)和覆盖条件(covering condition)。注意生产者/消费者问题中的两个条件变量的用法以及使用while检查条件避免假唤醒(spurious wakeup)以及某些线程库的signal唤醒两个线程的情况。
信号量
信号量是一个根据值的正负来控制多线程访问临界区的变量。其支持的语义:
-
wait:让信号量的值减一。如果其值为负数,那么wait线程进行等待。
-
signal:让信号量的值加一。如果加之前的值为负数,那么唤醒一个等待线程。
基于事件循环的并发
除了多线程并发模型外,基于事件循环的并发模型也是一个常用的模型。其基础一般是事件循环+异步io。在事件循环的问题中,我们一般需要处理阻塞的问题以及使用异步io带来的状态管理问题。