如何提供对大型数组元素的线程安全访问

Posted

技术标签:

【中文标题】如何提供对大型数组元素的线程安全访问【英文标题】:How to provide thread-safe access to elements of a large array 【发布时间】:2020-06-10 03:29:44 【问题描述】:

我在内存中有一个非常大(数十 GB)的 mmap'd 数组,我需要提供线程安全访问。访问模式是频繁读取偶尔写入(添加)。只有几十个线程,所以碰撞的机会很低,但不是零。

我考虑过的事情:

我可以为每个元素创建一个互斥锁,但这在内存方面似乎很浪费。 我可以创建一个 unordered_set 来存储要写入的元素。如果它是一个共享锁,这并不可怕,但每次我写一个双精度时插入和删除在运行时开销方面似乎很重

我想知道是否有办法做类似于fetch_add 而不使所有双打原子(特别是因为atomic<double> 在C++ 20 之前大多数情况下都没有在C++ 中实现)。

有没有更好的方法来提供这种线程安全的访问?

【问题讨论】:

也许每 N 个元素有一个 std::shared_mutex?对不同的 N 值进行基准测试可能会发现一个效果很好的值。它不会是“完美的”(排他锁会阻止对未使用元素的访问),但处理可伸缩性时的目标通常不是“完美”而是“足够好”。 操作顺序重要吗?写入线程执行什么样的更新?读取线程是否需要数据的一致性以及任何不变量是否成立? atomic<double> 未实现”是什么意思?即使fetch_add 未实现,您也可以始终使用比较和交换操作。 写作线程只是做补充。我不确定如何使用atomic<double>,因为数组是 mmap 的 【参考方案1】:

您的问题中没有提及数据的一致性,这是最重要的方面。我将尝试描述几种可能的模式,也许其中一种会满足您项目的需求。

首先,让我们想象一下,您的巨大 mmap 中的双精度数据之间可能没有一致性:例如传感器数据,不需要传感器之间的一致性。您可以将atomic<double>atomic::store 操作一起使用:您只需存储和读取一些数据,唯一的一致性是每个元素都有新值或旧值(原子保证双精度值是一致的)。如果您需要fetch_add 操作,您可以随时使用compare_exchange_weak 自行实现。

下一个案例:记录应该有一些一致性。您需要定义记录是什么,并在每条记录的基础上拥有一个shared_mutex(或任何其他锁)。

另一种选择:假设您需要记录之间的一致性,但您不需要它们完全同步。类似于缓存数据的东西,这些数据最终会被更新(C++ 中的宽松内存模型)。您可能对实际数据有一个shared_ptr。每个读取线程都可以复制共享指针并获得要读取的数据的所有权(这是一种廉价的操作,C++ 保证线程安全)。读取数据后,线程必须销毁共享指针的副本并释放所有权。有趣的部分是写作线程。它将数据副本创建到“交换”内存中,并且该内存由独立的共享指针拥有。写入线程更新数据,它可能需要尽可能多的时间:读取线程不知道存在“交换”并且仍然使用旧数据。一旦写入线程完成它的工作,它就会存储shared_ptr 以将内存交换为实际的数据共享指针。

重要的是:在写入线程提交它的工作时,仍有可能使用旧数据读取线程。这不是问题——他们拥有旧数据的所有权,而这些数据只是稍微过时了。无论如何,新读者都会得到更新的数据。

【讨论】:

在语法上,我不确定如何将atomic<double> 与 mmap 结合使用。 shared_ptr 方法虽然很有趣 如果你有一个指向内存的指针,你可以使用 placement new 操作符来放置任何类型的对象。您可以使用placement new 创建一个原子值数组。

以上是关于如何提供对大型数组元素的线程安全访问的主要内容,如果未能解决你的问题,请参考以下文章

java并发 之构建线程安全程序

Java中如何保证线程安全性

ArrayList , Vector 源码理解

如何创建线程?如何保证线程安全?

具有并发调度组的线程安全读/写数组访问[重复]

java中的线程安全随机访问循环数组?