【问题标题】:Is mutex needed when all threads set flags in an array based on search result当所有线程根据搜索结果在数组中设置标志时是否需要互斥锁
【发布时间】:2022-01-17 15:18:01
【问题描述】:

我有一个看起来像这样的代码块

std::vector<uint32_t> flags(n, 0);
#pragma omp parallel for 
for(int i = 0; i <v; i++) {
   // If any thread finds it true, its true. 
   // Max value of j is n.
   for(auto& j : vec[i]) 
      flags[j] = true; 
}

Work based upon the flags.

是否需要互斥锁?我了解缓存一致性将确保所有写入缓冲区都是同步的,并且不会将冲突的缓冲区写入内存。其次,可以通过简单的改变来避免缓存一致性的开销

flags[j] = true; 

to 

if(!flags[j]) flags[j] = true; 

检查 flags[j] 是否已设置将降低写入频率,因此需要缓存一致性更新。即使有任何机会 flags[j] 被读取为 false,它也只会以对 flags[j] 的一次额外写入而告终,这是可以的。

编辑:

是的,多个线程可能并且将尝试写入 flags[j] 中的同一索引。因此问题。

uint32_t 已被有意使用,而 bool 未被使用,因为并行写入布尔值可能会发生故障,因为相邻的布尔值共享相同的字节。但是即使没有互斥体,从不同线程并行写入同一个 uint32_t 也不会像布尔值一样出现故障。

FWIW,为了符合标准,我最终保留了这个或多或少符合标准的代码,但不是 100%。但是上面显示的非标准代码在测试中没有失败。我一度认为它在多套接字机器中会失败,但结果证明 x86 也提供多套接字级别缓存一致性。

#pragma omp parallel 
{

std::vector<uint32_t> flags_local(n, 0);
#pragma omp parallel for
for(int i = 0; i <v; i++) {
   for(auto& j : vec[i]) 
     flags_local[j] = true; 
}

// No omp directive here, as all threads 
// need to traverse their full arrays.
for(int j = 0; j <n; i++) {
   if(flags_local[j] && !flags[j]) {
      #pragma omp critical 
      { flags[j] = true; }
   }
}

}

【问题讨论】:

  • nv是什么关系?
  • 是否有一个可能的j 属于vec[i1]vec[i2](所以并发写入flags[j])?
  • @YSC 假设 n 和 v 之间没有关系,但是 vec[i] 的元素的值,其中 i 从 0 到 v,最多可以等于 n。
  • @Jarod42,这就是为什么我很困惑是否应该使用互斥锁,但在这种情况下,两个线程都想写 true,如果只是一个成功了,它不像一个会取消另一个。
  • @Jarod42 请注意,您不能重新分配原子向量,因为原子类型既不可移动也不可复制。从C++20开始,这个问题可以用std::atomic_ref解决。

标签: c++ parallel-processing openmp mutex


【解决方案1】:

C++ 中的线程安全使得您无需担心缓存一致性和此类硬件相关问题。重要的是 C++ 标准中指定的内容,我认为它没有提到缓存一致性。这是实施者需要担心的。

对于写入std::vector 的元素,规则实际上相当简单:写入向量的不同元素是线程安全的。只有当两个线程写入同一个索引时,您才需要同步访问(为此,两个线程是否写入相同的值并不重要)。


正如 Evg 所指出的,我做了一个粗略的简化。重要的是所有线程访问不同的内存位置。因此,对于 std::vector&lt;bool&gt;,事情不会那么简单,因为通常 std::vector&lt;bool&gt; 的多个元素都存储在一个字节中。


是的,多个线程可能并且将尝试写入 flags[j] 中的同一索引。

然后你需要同步访问。所有元素最初都是false 并且所有写入都写入true 的事实不相关。重要的是您有多个线程访问相同的内存并且至少有一个线程对其进行写入。在这种情况下,您需要同步访问权限,否则会出现数据争用。

【讨论】:

  • 除非是std::vector&lt;bool&gt;
  • 感谢@Evg,是的,这就是我专门使用 uint32_t 的原因
  • @Oner 有什么理由不使用uint8_t 而不是uint32_t
  • @DanielLangr,在其他实验中,我发现 uint8_t 由于缓存一致性而引入了比 uint32_t 更多的开销,并且执行速度要慢得多。
  • @Oner 好的,这可能是由于 错误共享​​> 问题。
【解决方案2】:

同时访问变量以进行写入是一种竞争条件,因此它是未定义的行为。

flags[j] = true;

应该受到保护。

您也可以使用原子类型(但请参阅how-to-declare-a-vector-of-atomic-in-c++)。

使用std::atomic_ref (c++20) 甚至更简单

std::vector<uint32_t> flags(n, 0);
#pragma omp parallel for 
for (int i = 0; i < v; i++) {
   for (auto& flag : vec[i]) {
       auto atom_flag = std::atomic_ref<std::uint32_t>(flag);
       atom_flag = true; 
   }
}

【讨论】:

  • 写入 32 位整数是原子的。即使它不是原子的,两个线程都试图写入相同的值。所以我认为比赛条件应该没问题。
  • @Oner 对于某些特定的体系结构可能是这样,但通常不是这样。请注意,即使是未对齐的 32 位写入,在 x86_64 上也可能不是原子的。此外,如果没有原子,as-if 规则的效果可能会完全优化读取。
  • @Oner C++ 标准与硬件无关,它说uint32_t 不是原子的。
  • 使用 OpenMP,您甚至不需要 std::atomic_ref。我们有#pragma omp atomic write(可能在这里放松)。
  • @463035818_is_not_a_number 我自己使用了另一个安全的代码块,出于好奇,我发布了这个问题。现在假设写入 uint32_t 不是原子的,一个线程写入前 16 位,另一个线程写入后 16 位。但是他们写的是相同的值。是的,有一个竞争条件,但它的影响被绕过了,因为没有线程会写假。所以最后,如果它的意思是真实的,它应该是真实的。还是我错过了其他任何东西。
猜你喜欢
  • 2010-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-17
  • 2011-12-01
  • 2015-08-25
  • 1970-01-01
  • 2011-09-06
相关资源
最近更新 更多