8.1 临界区和竞争条件
所谓临界区就是访问和操作共享数据的代码段。如果两个执行线程有可能处于同一个临界区中,就称它是竞争条件。避免并发和防止竞争条件被称为同步。
8.2 加锁
各种锁机制之间的区别主要在于当锁被争用时(已经被使用)的行为表现--一些锁会简单地执行忙等待,而有些锁会使当前任务睡眠直到锁可用为止。
8.2.1 到底是什么造成了并发执行
用户空间之所以需要同步,是因为用户程序会被调度程序抢占和重新调度。
内核中有类似可能造成并发执行的原因。它们是:
- 中断--中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码。
- 软中断和tasklet--内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码。
- 内核抢占--因为内核具有抢占性,所以内核中的任务可能会被另一任务抢占。
- 睡眠及与用户空间的同步--在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行。
- 对称多处理--两个或多个处理器可以同时执行代码。
8.2.2 要保护些什么
执行线程的局部数据仅仅被它本身访问,显然不需要保护,比如,局部自动变量(还有动态分配的数据结构,其地址仅存放在堆栈中)不需要任何形式的锁,因为它们独立存在于执行线程的栈中。类似,如果数据只会被特定的进程访问,那么也不需要加锁(因为进程一次只在一个处理器上执行)。
记住,要给数据而不是代码加锁。
8.3 死锁
死锁的产生需要一定条件:要有一个或多个执行线程和一个或多个资源,每个线程都在等待其中的一个资源,但所有的资源都已经被占用了。
一下一些简单的规则对避免死锁大有帮助:
- 加锁的顺序是关键。使用嵌套的锁时必须保证以相同的顺序获取锁这样可以阻止致命拥抱类型的死锁。
- 防止发生饥饿,试问,这个代码的执行是否一定会结束?
- 不要重复请求同一个锁。
- 越复杂的加锁方案越有可能造成死锁--设计应力求简单。
最值得强调的是第一点,它最重要。如果有两个或多个锁曾在同一时间里被请求,那么以后其他函数请求它们也必须按照前次的加锁顺序进行。
注意,尽管释放锁的顺序和死锁是无关的,但最好还是以获得锁的相反顺序来释放锁。
8.4 争用和扩展性
当锁争用严重时,加锁太粗会降低可扩展性;而锁争用不明显时,加锁过细会加大系统开销,带来浪费,这两种情况都会造成系统性能下降。