多线程编程指南

避免死锁

死锁是指永久阻塞一组争用一组资源的线程。仅因为某个线程可以继续执行,并不表示不会在某个其他位置发生死锁。

导致死锁的最常见错误是自死锁递归死锁。在自死锁或递归死锁中,线程尝试获取已被其持有的锁。递归死锁是在编程时很容易犯的错误。

例如,假设代码监视程序让每个模块函数在调用期间都获取互斥锁。随后,由互斥锁保护的模块内函数间的任何调用都会立即死锁。函数调用模块外的代码时,如果迂回调入任何受同一互斥锁保护的方法,则该函数也会发生死锁。

这种死锁的解决方案就是避免调用模块外可能通过某一路径依赖此模块的函数。需要特别指出的是,应避免在未重新建立不变量的情况下调用回调入模块的函数,而且在执行调用之前不要删除所有的模块锁。当然,在调用完成和重新获取锁定之后,必须验证状态,以确保预期的操作仍然有效。

另一种死锁的示例就是当两个线程(线程 1 和线程 2)分别获取互斥锁 A 和 B 时的情况。假设,线程 1 尝试获取互斥锁 B,线程 2 尝试获取互斥锁 A。如果线程 1 在等待互斥锁 B 时受到阻塞,而无法继续执行。线程 2 在等待互斥锁 A 时受到阻塞也无法继续执行。任何情况都不会发生变化。因此,在这种情况下,将永久阻塞线程,即出现死锁现象。

通过建立获取锁定的顺序(锁定分层结构),可以避免这种死锁。当所有线程始终按指定的顺序获取锁定时,即可避免这种死锁。

遵循严格的锁定获取顺序并不总是非常理想。例如,线程 2 具有许多有关在持有互斥锁 B 时模块状态的假设。放弃互斥锁 B 以获取互斥锁 A,然后按相应的顺序重新获取互斥锁 B 将导致线程放弃其假设。必须重新评估模块的状态。

阻塞同步元语通常具有变体,这些变体将尝试获取锁定,并在无法获取锁定时失败。例如 mutex_trylock()。元语变体的这种行为允许线程在不出现争用时破坏锁定分层结构。出现争用时,通常必须放弃持有的锁定,并按顺序重新获取锁定。

与调用相关的死锁

由于不能保证获取锁定的顺序,因此如果特定线程永远不能获取锁定就会出现问题。

持有锁的线程释放锁,一小段时间后重新获取锁定时,通常会出现此问题。由于锁被释放,因此其他线程似乎理应可以获取锁。但是,持有锁的线程未被阻塞。因此,从线程释放锁到重新获取锁定的时间内,该线程将持续运行。这样,就不会运行其他线程。

通常,通过在进行重新获取锁定的调用前调用 thr_yield(3C),可以解决此类型的问题。thr_yield() 允许其他线程运行并获取锁定。

由于应用程序的时间片要求是可变的,因此系统不会强加任何要求。可通过调用 thr_yield() 来使线程根据需要进行分时操作。

锁定原则

请遵循以下的简单锁定原则。