单个进程中有数千个读/写锁

Thousands of reader/writer locks in a single process

本文关键字:千个 写锁 进程 单个      更新时间:2023-10-16

我目前正在设计一个具有大规模同步模式的c++跨平台(Linux/Windows)服务器应用程序。我在内部使用boost::thread作为操作系统特定线程的抽象。我的问题是保护一个数据数组,数组的每个元素被一个独立的读/写锁保护。

我的数组包含4096个元素考虑到"信号量小书"(第85页)中提出的"写入器优先级读取器写入器"问题的解决方案,我的应用程序每个数组元素需要5个信号量。这总共提供了大约20000个信号量(或者,相当于20000个互斥锁+ 20000个条件变量)。

我的应用程序的另一个特点是,在给定的时间内,大多数信号量都不是活动的(通常有大约32个"客户端"线程在数千个信号量上等待/发信令)。请注意,由于整个服务器运行在单个进程中,所以我使用轻量级的、基于线程的信号量(而不是进程间信号量)。

我的问题是双重的:

  1. 是否建议在Linux和Windows上为单个进程创建总计20000个信号量 ?嗯,当然,我想情况并非如此……

  2. 如果不推荐这种做法,我可以使用什么技术来减少实际信号量的数量,例如,在1个实际信号量的顶部创建N个"模拟信号量"的集?我想这将是一个有趣的解决方案,因为我的大多数信号量在给定时间是不活动的。

提前感谢!

到目前为止的答案总结

  1. 不建议使用数千个信号量,特别是从跨平台的角度来看。因此,即使它们不是进程间信号量(它们仍然在Windows下消耗句柄)。
  2. 解决我的问题的一个直接方法是将我的数组拆分为64个子数组,例如16个元素,并将每个子数组与单个读/写锁相关联。不幸的是,这会带来很多争用(1个写入器会阻塞对15个元素的读取)。
  3. 深入Boost源代码,我发现:

    • "boost::mutex"的实现不包装windows_critical_section对象(但CreateEvent和ReadWriteBarrier),
    • "boost::shared_mutex"在Windows下使用CreateSemaphore(这是重量级的进程间对象),并且
    • "boost::shared_mutex"不封装"pthread_rwlock_t"
    这个的原因我不太清楚。特别是,在Windows下使用"boost::shared_mutex"的进程间对象对我来说似乎不是最优的。

目前开放问题总结

  1. 我如何在1个实际信号量的顶部创建一组N个"模拟信号量",使模拟信号量之间的争用尽可能小?
  2. 如何"boost::mutex"answers"boost::shared_mutex"与他们的本地对应(CRITICAL_SECTION和pthread_rwlock_t)比较?
  1. 不建议这样做。你不应该这么做,因为在Windows中,每个信号量消耗1个Handle Object。一个过程只能管理特定数量的句柄对象。线程/进程和其他Windows对象可能需要使用Handle对象和will如果他们做不到,就会崩溃。这与Linux中的文件描述符的概念。

  2. 将4096个元素分成30组(例如)140个元素元素,并为每个140组分配一个信号量。然后30(在本例中)线程将尝试访问这30个集合和

我将从Windows的角度告诉你我对它的看法。我在编写Windows服务器应用程序方面很有经验。

首先,为单个进程创建20k个信号量绝对没有问题。它是一个非常轻量级的内核对象。甚至"进程间"信号。

我看到你的设计还有一个问题。您应该知道在内核对象(如信号量/互斥锁)上执行的每个操作都涉及一个繁重的内核模式事务(也称为系统调用)。每次这样的调用可能会消耗大约2k个CPU周期,即使根本没有碰撞。

因此,您可能会发现自己处于这样一种情况,即大部分处理器时间都花在了同步方法的调用上。

相反,为了同步线程,可以使用互锁操作。它们的成本要低得多(通常是几十个CPU周期)。

还有一个对象叫做临界区。它是一种互锁操作数和内核对象的混合体(在实际发生碰撞时使用)。您应该检查通常将元素锁定多长时间。如果它通常是一个短持续时间的锁-只需使用临界区,忘记复杂的读写锁。

如果您处理长时间锁,并且确实需要一个读写锁,并且您发现您在内核模式事务中花费了大量CPU -考虑创建您自己的(或尝试找到一个现有的)这种锁的混合实现。

在Linux上,您绝对不应该自己实现锁,而应该使用posix_rwlock_t

拥有一个由4096这样的元素组成的数组应该不会出现任何特别的问题。POSIX锁结构在Linux中实现得相当有效。特别是,它们在可能的"快速路径"上使用原子操作,并且仅在特定锁上出现拥塞时才进入系统调用(特别是对于FUTEX)。因此,如果您相对小心地实现,使任何线程一次只持有1或2个锁,那么Linux上的约束将仅由工作线程的总数而不是对象本身的数量给出。