6.4 同步机制
如第6.2节和第6.3节所述,OS平台初步提供了一些同步机制,允许进程和线程等待其他进程和线程结束。对那些并发执行多个独立执行路径、具有较低精度的应用程序来说,这些机制已经够用了。但是,很多并发应用程序需要更精细的同步机制,以允许进程和线程协调它们的执行次序和访问共享资源(如文件、数据库记录、网络设备、对象成员、共享内存)的次序。为了防止“竞态条件(race condition)”的发生,对这些资源的访问必须保持同步。
当两个或多个并发线程的执行次序造成了意想不到的错误结果时,“竞态条件”就会产生。防止“竞态条件”的一个方法是使用同步机制(synchronization mechanisms),对访问“共享资源”的代码中的关键段(critical section)实施“串行访问”控制。常用的OS同步机制有:互斥体(mutex)、“多读取者/单写入者”锁(readers/writer locks)、信号量(semaphores)和条件变量(condition variable)。
为了说明这些机制的必要性,请看看在第4.4.3节所定义的Iterative_Logging_Server::handle _data()方法中,我们添加以下代码:
typedef u_long COUNTER;
// File scope global variable.
static COUNTER request_count;
// ...
virtual int handle_data (ACE_SOCK_Stream *) {
while (logging_handler_.log_record () != -1)
// Keep track of number of requests.
++request_count;
ACE_DEBUG ((LM_DEBUG,
"request_count = %d\n", request_count));
logging_handler_.close ();
return 0;
}
handle_data()方法等待客户日志记录的到来。当一个记录到达时,这个记录会被删除,并会接受处理。request_count变量用来记录“接收到的客户日志记录”的数目。
只要handle_data()是在“一个进程中的一个线程中”执行,上面的代码就会正常工作。但是,在很多平台上,当多个线程在同一个进程中同时执行handle_data()时,会造成不正确的结果。问题出在:全局变量request_count上存在“竞态条件”,因而,这段代码不是“线程安全(thread-safe)”的。特别是,在具有以下特点的平台上,不同的线程会去递增、打印requeset_count变量的“陈旧”版本:
l “自增(auto-increment)”操作中编译进(compile into)了多个汇编语言操作,如load、add和store赋值”;
l 整个内存的排序没有通过硬件得以保证。
为保证上述以及类似情况下,程序会得到正确的结果,操作系统提供了一些同步机制。以下是最常见的同步机制。
6.4.1 互斥体(Mutex,Mutual Exclusion)锁
当共享资源被多个线程并发访问时,为了确保这些资源的完整性,我们可以使用互斥体(mutex)锁。互斥体可用来串行执行多个线程,这需要在代码中确定关键段(critical section)——即,一次只能由一个线程执行的代码。“互斥体”语义像双括号那样具有“对称性”;也就是说,如果一个线程拥有互斥体,那么,它还得负责释放这个互斥体。这种简单的语义有助于保证互斥体的高效实现——如通过硬件自旋锁(spin lock)来实现。2
常见的互斥体有两种:
l 非递归互斥体(nonrecursive mutex)——如果当前拥有互斥体的线程在没有首先释放它的情况下,试图再次获得它,就会导致死锁或失败;
l 递归互斥体(recursive mutex)——拥有互斥体的线程可以多次获得它而不会产生自死锁,只要这个线程最终以相同次数释放这个互斥体即可。
6.4.2 Readers/Writer锁
“Readers/writer(多读取者/单写入者)锁”可以通过以下方式之一访问共享资源:
l 多个线程并发读取资源,但不修改它;
l 一次只有一个线程修改资源,此时其他线程都不能对其进行读/写访问。
“readers/writer锁”可用来保护“读操作比写操作频繁”的资源,从而提高并发应用程序的性能。在实现“readers/writer锁”时,要么给“读取者”以优先权,要么给“写入者”以优先权[BA90]。
“readers/writer锁”和互斥体有某些共性,例如:获得锁的线程也必须释放这个锁。如果一个“写入者”希望获得这个锁,那么,这个线程必须等待其他所有“拥有这个锁”的线程释放它;然后,这个“写入者”线程单独占有这个锁。但和互斥体不同的是,多个线程可以同时获得一个“readers/writer锁”执行“读”操作。
6.4.3 信号量锁
从概念上说,信号量(semaphore)是可以原子(atomically)递增和递减的非负整数。如果一个线程试图递减一个信号量,但这个信号量的值已经为0,则线程将会阻塞;另一个线程“发出(post)”这个信号(semaphore),使信号量的值大于0之后,被阻塞的线程才会被释放。
信号量维护状态信息,对信号计数值(count)和被阻塞线程的数量进行记录。它们一般是通过“休止锁(sleep lock)”来实现的;休止锁用来触发环境切换,以允许其他线程执行。和互斥体不同的是,释放信号量的线程不必是最初获得这个信号量的线程。这使得信号量适用于更广泛的执行环境,如信号处理程序或中断处理程序。
6.4.4 条件变量
和“互斥体锁”、“readers/writer锁”、“信号量锁”不同,条件变量(condition variable)提供了不同风格的同步方式。在前三种机制中,当“占有锁的线程”在关键段中执行代码时,其他线程会等待。与此相反,使用条件变量,线程可以调整和调度自己的处理过程。
例如,某一数据被其他线程共享,条件变量可以使自己处于等待状态,直至“涉及这个数据”的一个条件表达式达到某一状态。当一个“合作线程(cooperating thread)”显示共享数据的状态已经改变时,阻塞在条件变量上的线程会被唤醒。然后,被唤醒的线程重新计算它的条件表达式,如果共享数据达到预期状态,则恢复处理。再复杂的条件表达式也可以通过条件变量来等待,所以,较之前面所说的同步机制,条件变量允许更复杂的调度决策。
条件变量为高级并发模式(如Active Object和Monitor Object [SSRB00])不仅提供了核心同步机制,还提供了“进程内”通信机制(如同步消息队列)。Pthreads和(实现在Solaris之上的)
UNIX International(UI)线程支持条件变量,但其他平台(如Win32和很多实时操作系统)不支持条件变量。
在副栏12中,针对上文介绍的OS同步机制,就其性能特征作了比较和评估。一般来说,应该尽量使用那些“提供了所需语义”、“最有效率”的同步机制。如果效率很重要,就得针对你的应用程序的目标平台,找到相应的信息。然后,可以通过变换使用各种ACE“同步”类来调整你的应用程序,以充分利用平台的差异性。而且,通过使用Strategized Locking模式 [SSRB00],并遵循第10.1节介绍的ACE_LOCK*伪类(pseudo-class)的方法签名(method signature),其代码还可以保持可移植性。
|
副栏12:同步机制的性能评估 |
|
操作系统提供了一些同步机制,用以满足不同应用程序的需要。虽然根据具体的OS实现和硬件的不同,本章讨论的同步机制在性能上各有差异,但以下常识一定要知道: l 和互斥体相比,条件变量和信号量的开销往往更大,因为它们的实现更为复杂。但是,和那些用户创建的东西相比,原始OS机制总是表现得更出色,因为后者可以利用“OS的内部特性”和“专门针对硬件的优化”。 l 和“readers/writer锁”相比,互斥体的开销通常更低,因为它们不需要管理多个“等待者集(waiter set)”。但是,多个“读取者”线程可以并行执行;所以,在多处理器上,如果将“readers/writer锁”用于访问那些“读操作”比“写操作”多的数据,“readers/writer锁”的伸缩性会表现得更好。 l 和递归互斥体相比,非递归互斥体效率更高。此外,如果程序员忘记释放递归互斥体,就会造成不易察觉的编程错误 [But97];但如果忘记释放的是非递归互斥体,则系统会立即通过“死锁”来暴露这些问题。 |






