非死锁缺陷
违反原子性缺陷
定义:违反了多次内存访问中预期的可串行性。
这个例子中,两个线程都要访问thd结构中的成员proc_info。第一个线程检查proc_info非空,然后打印出值;第二个线程设置其为空。显然,当第一个线程检查之后,在fputs调用之前被中断,第二个线程把指针置为空;当第一个线程恢复执行时,由于引用空指针,导致程序奔溃。
解决方案:加锁。我们只要给共享变量的访问加锁,确保每个线程访问proc_info字段时,都持有锁。访问这个结构的所有其他代码,也应该先获取锁。
违反顺序缺陷
定义:两个内存访问的预期顺序被打破了。
线程2的代码中似乎假定变量mThread已经被初始化了。然而,如果线程1并没有首先执行,线程2就可能因为引用空指针奔溃。
解决方案:条件变量。
在这段修复的代码中,我们增加了一个锁、一个条件变量以及状态的变量。初始化代码运行时,会将mtInit设置为并发出信号表明它已做了这件事。如果线程2先运行,就会一直等待信号和对应的状态变化;如果后运行,线程2会检查是否初始化,然后正常运行。请注意,我们可以用mThread本身作为状态变量,但为了简洁,我们没有这样做。当线程之间的顺序很重要时,条件变量能够解决问题。
死锁缺陷
当线程1占有锁L上下文切换到线程线程2锁住L试锁住L这时才产生了死锁,两个线程互相等待。如所示,其中的圈表明了死锁。
产生死锁的条件
互斥:线程对于需要的资源进行互斥的访问。持有并等待:线程持有了资源,同时又在等待其他资源。非抢占:线程获得的资源,不能被抢占。循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源又是下一个线程要申请的。
循环等待
让代码不会产生循环等待。最直接的方法就是获取锁时供一个全序。假如系统共有两个锁,那么我们每次都先申请L1然后申请L就可以避免死锁。这样严格的顺序避免了循环等待,也就不会产生死锁。
持有并等待
死锁的持有并等待条件,可以通过原子地抢锁来避免。
先抢到prevention这个锁之后,代码保证了在抢锁的过程中,不会有不合时宜的线程切换,从而避免了死锁。这需要任何线程在任何时候抢占锁时,先抢到全局的prevention锁。例如,如果另一个线程用不同的顺序抢锁L1和L也不会有问题,因为此时,线程已经抢到了prevention锁。
非抢占
在调用unlock之前,都认为锁是被占有的,多个抢锁操作通常会带来麻烦,因为我们等待一个锁时,同时持有另一个锁。很多线程库供更为灵活的接口来避免这种情况。具体来说,trylock函数会尝试获得锁,或者返回−表示锁已经被占有。
通过强大的硬件指令,我们可以构造出不需要锁的数据结构。从而完全避免互斥。
通过调度避免死锁
例如,假设我们需要在两个处理器上调度4个线程。更进一步,假设我们知道线程1需要用锁L1和LT2也需要抢L1和LT3只需要LT4不需要锁。我们用表32来表示线程对锁的需求。
一种比较聪明的调度方式是,只要T1和T2不同时运行,就不会产生死锁。下面就是这种方式:
请注意,T3和T1重叠,或者和T2重叠都是可以的。虽然T3会抢占锁L但是由于它只用到一把锁,和其他线程并发执行都不会产生死锁。
我们再来看另一个竞争更多的例子。在这个例子中,对同样的资源有更多的竞争。锁和线程的竞争如表33所示:
特别是,线程TT2和T3执行过程中,都需要持有锁L1和L下面是一种不会产生死锁的可行方案:
你可以看到,TT2和T3运行在同一个处理器上,这种保守的静态方案会明显增加完成任务的总时间。尽管有可能并发运行这些任务,但为了避免死锁,我们没有这样做,付出了性能的代价。
小结
文章为作者独立观点,不代表股票交易接口观点