10.2 对(代码)块的同步访问:临界区
对于大多数同步需求来说,单个原子操作是不够的,它们往往需要对所谓的临界区的独占访问[Bulk1999]。例如,如果你有两个变量需要原子地进行更新,你就必须使用一个同步对象来确保每个线程对该临界区的访问都是独占地进行的,像这样:
- // 共享对象
- SYNC_TYPE sync_obj;
- SomeClass shared_instance;
- . . .
- // 被多线程调用的代码段
- lock(sync_obj);
- int i = shared_instance.Method1(. . .);
- shared_instance.Method2(i + 1, . . .);
- unlock(sync_obj);
Method1()和Method2()这两个操作必须以一种不被中断的顺序进行。因此,调用它们的代码被包裹在获取和释放同步对象之间。通常,任何对此类同步对象的使用的代价都是高昂的,因而需要某些途径来使这些开销最小化甚至完全避免。
将同步对象用于保护临界区的做法之所以代价高昂,原因有二:第一,同步对象的使用本身可能代价高昂。例如,考虑表10.1中的时间(以毫秒为单位),它们是基于对4个Win32同步对象的1000万次"获取-释放"周期,以及由两个空函数调用所代表的控制场景。结果一目了然地显示出来,使用同步对象的代价是相当可观的,可能高达常规函数调用开销的150倍!
表10.1
|
同 步 对 象< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
单 处 理 器 |
对称多处理器 |
|
无(控制) |
117 |
172 |
|
CRITICAL_SECTION |
1740 |
831 |
|
atomic_integer |
1722 |
914 |
|
互斥体 |
17891 |
22187 |
|
信号量 |
18235 |
22271 |
|
事件 |
17847 |
22130 |
将同步对象用于保护临界区的第二个开销是由那些被拒绝在临界区之外的线程所导致的。临界区越长,就越是可能带来这种代价,因而最好尽量保持临界区的短小,或者将它们分解为多个"子临界区",就像在6.2节讨论的那样。然而,由于获取和/或释放同步对象的函数调用的代价可能会非常高,所以在临界区分解和导致"被拒"线程的长时间等待之间必须求得一个微妙的平衡。只有根据实际情况对性能进行评测你才能得到一个确定性的答案。