9.1临界区与竞争条件
临界区:访问和操作共享数据的代码段
多个线程并发访问一个资源通常是不安全的,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行。也就是说操作在执行结束前不可被打断。
竞争条件是一个bug,是两个执行线程处于同一个临界区时,两个执行线程同时执行了(要解决bug必须2选1,也就出现了竞争)。
同步是避免并发和防止竞争条件
1)为什么临界区需要保护
2)单个变量
9.2加锁
硬件:体系结构会提供一些实现算术和比较之类的原子操作。
软件:Linux内核程序员设计了锁机制。
加锁开锁硬件提供了原子操作。避免加锁开锁时产生竞争。
1)造成并发执行的原因
a)用户空间伪并发:
如果进程A进入了临界区,此时调度进程B抢占进程A,进程B随后进入了与进程A相同的临界区,进程A与B相互之间就会产生竞争。
信号处理是异步的,如果进程A进入了临界区,此时产生了信号需要马上处理,处理函数刚好要访问与进程A相同的临界区,进程A与信号相互之间就会产生竞争。
以上两种情况实际只是交叉执行,所以是伪并发。
b)用户空间真并发:
如果一台支持对称多处理器的机器,那么进程就可以正真地在临界区中同时执行
c)内核中有类似可能造成并发执行的原因,它们是:
中断----中断几乎可以在任何时刻异步发生,也就可能随时打断当前正在执行的代码
软中断和tasklet----内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在执行的代码
内核抢占----因为内核具有抢占型,所以内核中的任务可能会被另一任务抢占
睡眠及与用户空间的同步----在内核执行的进程可能会睡眠,这就会唤醒调度程序,从而导致调度一个新的用户进程执行
对称多处理----两个或多个处理器可以同时执行代码
真正困难的就是发现上述的潜在并发执行的可能,并有意识地采取某些措施来防止并发执行。
2)了解要保护些什么
a)找出哪些数据需要保护是关键所在:
线程中的局部数据仅仅被它访问,显然不需要保护。还有动态分配的数据结构,其地址在堆栈中。也不需要加锁
大多数内核数据结构都需要加锁
如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁。
如果任何其他什么东西都能看到它,那么就要锁住它。记住:要给数据而不是给代码加锁。
b)在编写代码时,你要问自己下面这些问题:
这个数据是不是全局的?除了当前线程外,其他线程能不能访问它。
这个数据会不会在进程上下文和中断上下文中共享?它是不是要在两个不同的中断处理程序中共享?
进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
当前进程是不是会睡眠(阻塞)在某些资源上,如果是,它会让共享数据处于核中状态?
怎样防止数据失控?
如果这个函数又在另一个处理器上被调度将会发生什么呢?
如何确保代码原理并发威胁呢?
简而言之,所有内核全局变量和共享数据都需要某种形式的同步方法。
9.3死锁
a)自死锁和ABBA死锁
b)预防死锁
9.4争用和扩展性
锁的争用:简称锁争用,是指当锁被占用时,有其他线程试图获得该锁。
加锁粒度:用来描述加锁保护的数据规模。
如何可以被计量的计算机组件都可以涉及可扩展性。
当锁争用严重时,加锁太粗会降低可扩展性;当锁争用不明显时,加锁过细会加大系统无畏开销。
即当 试图获得某线程占用的锁 的线程数量非常多时,加锁保护的数据规模太大会降低可扩展性,就是例如扩展了几个处理器,性能提高效果很小。
当 试图获得某线程占用的锁 的线程数量非常少时,加锁保护的数据规模太小会加大系统无畏开销,就是处理器数量少,单位时间产生并发访问的次数少,加锁保护的数据规模太小导致产生了很多的锁,很多锁没有用于应对并发,只是无畏的执行了锁操作。