线程中第二高效的元语是条件变量。条件变量的基本用途是基于状态的变化进行阻塞。条件变量可提供线程等待功能。请注意,线程在基于条件变量阻塞之前必须首先获取互斥锁,在从pthread_cond_wait()返回之后必须解除锁定互斥锁。线程还必须在状态发生改变期间持有互斥锁,然后才能对pthread_cond_signal()进行相应的调用。
信号比条件变量占用更多内存。由于信号变量基于状态而非控制来工作,因此在某些情况下更易于使用。与锁不同,信号没有属主。任何线程都可以增加已阻塞的信号。
通过读写锁,可以对受保护的资源进行并发读取和独占写入。读写锁是可以在读取或写入模式下锁定的单一实体。要修改资源,线程必须首先获取互斥写锁。必须释放所有读锁之后,才允许使用互斥写锁。
1、避免死锁
死锁是指永久阻塞一组争用一组资源的线程。仅因为某个线程可以继续执行,并不表示不会在某个其他位置发生死锁。
导致死锁的最常见错误是自死锁或递归死锁。在自死锁或递归死锁中,线程尝试获取已被其持有的锁。递归死锁是在编程时很容易犯的错误。
例如,假设代码监视程序让每个模块函数在调用期间都获取互斥锁。随后,由互斥锁保护的模块内函数间的任何调用都会立即死锁。函数调用模块外的代码时,如果迂回调入任何受同一互斥锁保护的方法,则该函数也会发生死锁。这种死锁的解决方案就是避免调用模块外可能通过某一路径依赖此模块的函数。需要特别指出的是,应避免在未重新建立不变量的情况下调用回调入模块的函数,而且在执行调用之前不要删除所有的模块锁。当然,在调用完成和重新获取锁定之后,必须验证状态,以确保预期的操作仍然有效。
另一种死锁的示例就是当两个线程(线程1和线程2)分别获取互斥锁A和B时的情况。假设,线程1尝试获取互斥锁B,线程2尝试获取互斥锁A。如果线程1在等待互斥锁B时受到阻塞,而无法继续执行。线程2在等待互斥锁A时受到阻塞也无法继续执行。任何情况都不会发生变化。因此,在这种情况下,将永久阻塞线程,即出现死锁现象。通过建立获取锁定的顺序(锁定分层结构),可以避免这种死锁。当所有线程始终按指定
的顺序获取锁定时,即可避免这种死锁。
遵循严格的锁定获取顺序并不总是非常理想。例如,线程2具有许多有关在持有互斥锁B时模块状态的假设。放弃互斥锁B以获取互斥锁A,然后按相应的顺序重新获取互斥锁B将导致线程放弃其假设。必须重新评估模块的状态。
2、与调用相关的死锁
由于不能保证获取锁定的顺序,因此如果特定线程永远不能获取锁定就会出现问题。持有锁的线程释放锁,一小段时间后重新获取锁定时,通常会出现此问题。由于锁被释放,因此其他线程似乎理应可以获取锁。但是,持有锁的线程未被阻塞。因此,从线程释放锁到重新获取锁定的时间内,该线程将持续运行。这样,就不会运行其他线程。
通常,通过在进行重新获取锁定的调用前调用thr_yield(3C),可以解决此类型的问题。thr_yield()允许其他线程运行并获取锁定。由于应用程序的时间片要求是可变的,因此系统不会强加任何要求。可通过调thr_yield()来使线程根据需要进行分时操作。
3、锁定原则
请遵循以下的简单锁定原则。
■ 请勿尝试在可能会对性能造成不良影响的长时间操作(如I/O)中持有锁。
■ 请勿在调用模块外且可能重进入模块的函数时持有锁。
■ 一般情况下,请先使用粗粒度锁定方法,确定瓶颈,并在必要时添加细粒度锁定来缓解瓶颈。大多数锁定都是短期持有,而且很少出现争用。因此,请仅修复测得争用的那些锁定。
■ 使用多个锁定时,通过确保所有线程都按相同的顺序获取锁定来避免死锁。