【问题标题】:Threads writing to a buffer memory and thread memory写入缓冲区内存和线程内存的线程
【发布时间】:2015-09-17 18:46:09
【问题描述】:

假设您有一个正在运行的多线程程序。就我而言,该程序使用 std::thread ,但我想这并不重要。现在每个线程都需要写入某个全局缓冲区,但保证它们永远不会写入该缓冲区中的相同内存地址。问题:

  • 它是线程安全的吗?
  • 效率高吗?我的意思是,如果您有 8 个线程,它们可能会访问此缓冲存储器的不同部分。这可能会导致很多缓存未命中?

上面的代码只是我所说的“线程写入同一个缓冲区但从不在同一个地址”的一个示例(尽管在我的情况下缓冲区要大得多,并且线程访问该缓冲区的非常不同的部分) .

int *buffer = new buffer[10];
for (int i = 0; i < 10; ++i) {
    threads[k] = std::thread(threadFunc, std::ref(buffer), i);
}

void threadFunc(int *&buffer, const int &threadId)
{
    buffer[threadId] = threadId;
}

【问题讨论】:

  • 它绝对是安全的,但你有一个很好的观点,即缓存的一部分可能会依次为每个线程重新加载。我想你需要做一些时间测试。我想您可以安排所有线程与内存地址紧密绑定,确保所有线程在允许它们全部移动到下一个(隔离)块之前完成处理一个(隔离)块。不过听起来有点复杂。

标签: c++ multithreading c++11


【解决方案1】:

如果两个线程在没有同步的情况下执行冲突访问,多线程访问会产生未定义的行为(竞争条件或更糟)。

两次访问相同的内存位置冲突。访问单独的数组元素是安全的。

在标准中,1.7p3 这么说

一个内存位置要么是一个标量类型的对象,要么是一个最大的相邻位域序列,它们都具有非零宽度。 [注意:语言的各种特性,例如引用和虚函数,可能涉及程序无法访问但由实现管理的额外内存位置。 — 尾注] 两个或多个执行线程 (1.10) 可以更新和访问单独的内存位置,而不会相互干扰。

但是,它可能效率不高。如果多个数组元素适合单个缓存行,那么线程将争夺所有权。这被称为"false sharing",基本上否定了首先拥有缓存的性能优势。为了克服这个问题,可能需要添加填充,以便不同的数组元素存在于不同的缓存行中。

【讨论】:

  • 非常感谢您提到填充。难道不是另一种解决方案是让每个线程都有自己的本地缓存,然后在最后锁定缓冲区并写入主缓冲区?
  • @user18490:是的,如果从不共享每个线程的数据,这将有效。当跨线程访问发生但不频繁时,填充效果很好。
  • “如果多个数组元素适合单个缓存行,那么线程将争夺所有权。” – 仅当第二个硬件线程访问位于第一个硬件线程缓存上的内存位置时,才会发生错误共享。仅仅因为元素大小小于缓存行大小并不意味着存在自动错误共享。 (我假设你的意思是,你可能想澄清你的答案。)
  • @ArneVogel:确实存在退化的情况(一个硬件线程上下文在所有软件线程之间切换,或者恰好两个线程和数组恰好跨越缓存线边界),但问题显然是谈论每个数组元素由不同的线程写入,因此“多个数组元素适合单个缓存行”是“一个线程访问另一个线程缓存上的内存位置”的条件。
【解决方案2】:

...但保证它们永远不会写入相同的内存地址 这个缓冲区

这使它成为线程安全的。读取会很高效,但写入可能会在某些架构上造成缓存争用。

但是,缓冲区必须相当大,因为线程 ID 通常有数百个。你不能在没有同步的情况下使用像地图这样的策略,所以由于稀疏性,你必须使用像buffer[12]buffer[134]这样的稀疏数组。除非你为每个人分配一个自己的ID线程,这需要每个线程中的状态......这个想法被我下面的内容所取代。

最好的策略通常是让每个线程的堆栈保存一个指向静态内存位置的指针,并将这个上下文作为参数传递给线程内需要它的各种函数。这就是为什么上下文对象在多线程编程中如此有用的原因。

【讨论】:

  • “写入会非常高效”——不,由于错误共享可能会出现缓存争用
  • 为什么会引起争用?它应该对每个线程中的同一缓存行有不同的看法。
  • 因为a write requires the cache line to first be in "Exclusive" or "Modified"状态,不能同时缓存在多个线程中。
  • @Ben Voigt:在多核系统上,无论如何都有每核缓存。每个内核都有自己的 L1 和 L2 缓存。您描述的问题只有在两个线程共享相同的 L1 缓存时才有意义。
  • 我描述的问题恰好发生在有多个 L1 缓存时。一个核心上的写入会从所有其他缓存中驱逐该缓存行。
猜你喜欢
  • 1970-01-01
  • 2014-12-28
  • 1970-01-01
  • 1970-01-01
  • 2013-12-31
  • 2011-02-10
  • 1970-01-01
  • 1970-01-01
  • 2017-04-19
相关资源
最近更新 更多