当您查看任何类型的同步原语的“大小开销”时,请记住这些cannot包装得太紧密。之所以如此,是因为例如共享缓存行的两个互斥体如果同时使用,最终会导致缓存垃圾(错误共享),即使获取这些锁的用户从不“冲突”。 IE。想象两个线程运行两个循环:
for (;;) {
lock(lockA);
unlock(lockA);
}
and
for (;;) {
lock(lockB);
unlock(lockB);
}
与一个线程运行一个循环相比,在两个不同线程上运行时,您将看到迭代次数的两倍当且仅当两个锁是不在同一个缓存行内. If lockA
and lockB
都在同一个cacheline中,迭代次数每个线程将会减半 - 因为带有这两个锁的缓存行将在执行这两个线程的 CPU 核心之间永久地反弹。
因此,尽管实际数据大小自旋锁或互斥体底层的原始数据类型可能只是一个字节或一个 32 位字,有效数据大小这种物体的体积通常较大。
在断言“我的互斥体太大”之前请记住这一点。事实上,在 x86/x64 上,40 字节是太小为了防止错误共享,缓存行目前至少有 64 字节。
除此之外,如果您高度关注内存使用情况,请考虑通知对象不必是唯一的 - 条件变量可以触发不同的事件(通过predicate
that boost::condition_variable
知道)。因此,可以对整个状态机使用单个互斥体/CV 对,而不是每个状态一对。同样适用于例如线程池同步——拥有比线程更多的锁并不一定是有益的。
Edit:有关“错误共享”(以及在同一缓存行中托管多个原子更新变量所造成的负面性能影响)的更多参考,请参阅(除其他外)以下 SO 帖子:
- boost::detail::spinlock_pool 中的错误共享?
- 错误共享和 pthreads
- 错误共享和原子变量
如前所述,当在多核、每核缓存配置中使用多个“同步对象”(无论是原子更新的变量、锁、信号量……)时,允许每个对象有一个单独的缓存行空间。您在这里用内存使用来换取可扩展性,但实际上,如果您进入软件需要数百万个锁(需要 GB 内存)的区域,您要么有资金购买几百 GB 内存(以及一百个CPU核心),或者你在软件设计中做错了什么。
在大多数情况下(特定实例的锁/原子)class
/ struct
),只要包含原子变量的对象实例足够大,您就可以免费获得“填充”。