假设我有一个1,000,000个元素的数组,以及许多工作线程,每个线程操作此数组中的数据。工作线程可能正在使用新数据更新已填充的元素,但每个操作仅限于一个数组元素,并且与任何其他元素的值无关。
使用单个互斥锁来保护整个阵列显然会导致高争用。另一方面,我可以创建一个与原始数组长度相同的互斥锁数组,对于每个元素array[i]
,我会在操作时锁定mutex[i]
。假设数据均匀分布,这将主要消除锁争用,代价是大量内存。
我认为更合理的解决方案是拥有一组n
互斥量(其中1 mutex[i % n]
。如果n
足够大,我仍然可以最大限度地减少争用。
所以我的问题是,除了增加的内存使用量之外,以这种方式使用大量(例如> = 1000000)的互斥量是否会降低性能?如果是这样,在您开始看到退化之前,您可以合理使用多少互斥量?
我确信这个问题的答案有点特定于平台;我在Linux上使用pthreads。我也在努力建立自己的基准测试,但我正在研究的数据规模耗时,因此我们会对一些初步指导意见表示赞赏。
这是最初的问题。对于那些要求提供有关该问题的更详细信息的人,我有4个多GB二进制数据文件,描述了正在分析的5亿个事件附近的某个地方。有问题的数组实际上是支持非常大的链式哈希表的指针数组。我们将四个数据文件读入哈希表,如果它们共享某些特征,可能会将它们聚合在一起。现有实现有4个线程,每个线程读取一个文件并将该文件中的记录插入到哈希表中。哈希表有997个锁和997 * 9973 = ~10,000,000个指针。插入带有哈希h
的元素时,我首先锁定mutex[h % 997]
,然后在bucket[h % 9943081]
中插入或修改元素。这可以正常工作,据我所知,我们没有太多的争用问题,但是存在性能瓶颈,因为我们只使用16核机器的4核。 (因为我们的文件通常不是完全相同,因此更少。)一旦所有数据都被读入内存,我们就会分析它,它使用新线程和一个新的锁定策略调整到不同的工作量。
我试图通过切换到线程池来提高数据加载阶段的性能。在新模型中,我仍然为每个文件都有一个线程,它只是以~1MB块的形式读取文件,并将每个块传递给池中的工作线程进行解析和插入。到目前为止,性能提升很小,我所做的分析似乎表明锁定和解锁阵列所花费的时间可能是罪魁祸首。锁定内置于我们正在使用的哈希表实现中,但它允许指定独立于表大小使用的锁数。我希望在不改变哈希表实现本身的情况下加快速度。
答案 0 :(得分:3)
(对你的问题非常局部和可能是间接的答案。)
曾经尝试过(在CentOS上)将锁的数量从~1K的素数提高到~100的素数时获得了巨大的性能。虽然我从未完全理解其原因,但我最终想出(或只是说服自己)这是错误的问题。
假设您有一个长度 M 的数组,其中包含 n 工作者。此外,您使用散列函数来保护 M 元素 m< M 锁定(例如,通过一些随机分组)。然后,使用Square Approximation to the Birthday Paradox,两个工人之间发生碰撞的可能性 - p - 由下式给出:
p~n 2 /(2m)
因此,您需要的互斥锁数量 m 根本不依赖于 M - 它是 p 的函数和 n 。
答案 1 :(得分:2)
在Linux下,除了与更多互斥锁相关的内存之外,没有任何其他成本。
但是,请记住,互斥锁使用的内存必须包含在您的工作集中 - 如果您的工作集大小超过相关的缓存大小,您将看到显着的性能下降。这意味着您不需要过大的互斥锁阵列。
正如Ami Tavory所指出的,争用取决于互斥锁的数量和线程数,而不是受保护的数据元素的数量 - 因此没有理由将互斥锁的数量与数据元素的数量相关联(明显的条件是,更多互斥锁比元素更有意义。
答案 2 :(得分:0)
在一般情况下,我会建议
简单地锁定整个数组(简单,通常“足够好”,如果您的应用程序除了访问数组之外主要做“其他东西”)
...或......
在整个阵列上实现读/写锁定(假设读取等于或超过写入)
显然你的情景与这两种情况都不匹配。
问:你有没有考虑过实施某种“写队列”?最糟糕的情况是,您只需要一个互斥锁。最好的情况是,您甚至可以使用无锁机制来管理队列。在这里查看可能适用的一些想法:https://msdn.microsoft.com/en-us/library/windows/desktop/ee418650%28v=vs.85%29.aspx