[读写锁]
读写锁是专门为大多数读的情况设计的。在这种情况中,读写锁可以提供比互斥锁大得多的扩展性,因为互斥锁从定义上已经限制了任意时刻只能有一个线程持有锁,而读写锁允许任意多数目的读者线程同时持有读锁。
很多情况下读写锁仍然十分有用,比如当读者必须进行高延迟的文件或者网络 I/O 时.
读写锁的可扩展性显然说不上理想,临界区较小时尤其如此。为什么读锁的获取这么慢呢,这应该是由于所有想获取读锁的线程都要更新 pthread_rwlock_t 的数据结构。因此,一旦全部 128 个线程同时尝试获取读写锁的读锁,那么这些线程必须一个一个的更新读锁中的 pthread_rwlock_t 结构。最幸运的线程可能几乎立刻就获取读锁了,最倒霉的线程则必须等待其他 127个线程更新后才能获取读锁。增加 CPU 只能让这种情况变得更糟。假设你需要维护一个已分配结构数目的计数,来防止分配超过一个上限,呃,比如 10000。我们再进一步假设这些结构的生命周期很短,极少超出上限。
[原子操作]
任何对单一变量进行操作的原子操作都可以用“比较并交换”的方式实现,从这种意义上说,上述两个“比较并交换”的操作是 universal 的,虽然 bool 版本的原语在应用中效率更高。“比较并交换”操作通常可以作为其他原子操作的基础,不过这些原子操作通常存在复杂性、可扩展性和性能等诸方面问题。
__sync_synchronize()原语是一个“内存屏障”,它限制编译器和 CPU 对指令乱序执行的优化,详见第 12.2 节的讨论。在某些情况下,只限制编译器对指令的优化就足够了,CPU 的优化可以保留,此时就需要使用 barrier()原语。在某些情况下,只需要让编译器不优化某个内存访问就行了,此时可以使用 ACCESS_ONCE()原语,
比较 POSIX、gcc 原语和 Linux内核中使用的版本。精准的对应关系很难给出,因为 Linux 内核有各种各样的加锁、解锁原语,gcc 则有很多 Linux 内核中不能直接使用的原子操作。当然,一方面,用户态的代码不需要Linux 内核中各种类型的加锁、解锁原语,同时另一方面,gcc 的原子操作也可以直接用 cmpxchg()来模拟。
对于工具而言,如果 shell 脚本的 fork()/exec()开销(在 Intel 双核笔记本中最简单的 C 程序需要大概 480 毫秒)太大,那么使用 C 语言的 fork()和 wait()原语。如果这些原语的开销也太大(最小的子进程也需要 80 毫秒),那么你可能需要用 POSIX 线程库原语,选择合适的加锁、解锁原语和/或者原子操作。如果 POSIX 线程库原语的开销仍然太大(一般低于毫秒级),原语了。永远记住,进程内的通信和消息传递总是比共享内存的多线程执行要好。
[计数]
有一种可能实现上限计数器的方法是将 10000 的限制值平均划分给每个线程,然后给每个线程一个固定个数的资源池。假如有 100 个线程,每个线程管理一个有 100 个结构的资源池。这种方法简单,在有些情况下也有效,但是这种方法无法处理一种常见情况:某个结构由一个线程创建,但由另一个线程释放.一方面,如果线程释放一个结构就积一分的话,那么一直在分配的线程很快就分配光了资源池,而一直在释放的线程积攒了大量分数却无法使用。另一方面,如果每个释放的结构都能让分配它的 CPU 加一分,CPU 就需要操纵其他 CPU 的计数器,这将会带来很多代价昂贵的原子操作。
在最初的实现中,我们维护一个每线程计数器的上限值。当超过这个上限时,线程将自己的计数加到全局计数器上。当然,我们不能只简单地在分配结构时增加计数值:我们还必须在结构释放时减少计数值。因此我们必须在减少计数值时利用上全局计数器,否则每线程计数器的值就可能降到 0 以下。但是,如果这个上限足够大,那么几乎所有的加减操作都是在每线程计数器中执行的,这就给我们带来了良好的性能和可扩展性。
++操作符在 x86 上不是会产生一个 add-to-memory 的指令么?为什么 CPU 高速缓存没有把这个指令当成原子的?