【问题标题】:Data races, UB, and counters in C++11C++11 中的数据竞争、UB 和计数器
【发布时间】:2014-04-11 04:58:02
【问题描述】:

以下模式在许多想要告诉用户它做了多少次不同事情的软件中很常见:

int num_times_done_it; // global

void doit() {
  ++num_times_done_it;
  // do something
}

void report_stats() {
  printf("called doit %i times\n", num_times_done_it);
  // and probably some other stuff too
}

不幸的是,如果多个线程可以在没有某种同步的情况下调用doit,则对num_times_done_it 的并发读取-修改-写入可能是数据竞争,因此整个程序的行为将是未定义的。此外,如果 report_stats 可以在没有任何同步的情况下与 doit 并发调用,则在修改 num_times_done_it 的线程和报告其值的线程之间存在另一次数据竞争。

通常,程序员只希望以尽可能少的开销计算doit 被调用的次数的最正确计数。

(如果你认为这个例子很简单,Hogwild! 基本上使用这个技巧获得了比无数据竞争的随机梯度下降显着的速度优势。此外,我相信 Hotspot JVM 正是这种无人看管的多线程访问到方法调用计数的共享计数器——尽管它很清楚,因为它生成的是汇编代码而不是 C++11。)

明显的非解决方案:

  • 原子,我知道的任何内存顺序,在这里“尽可能少的开销”失败(原子增量可能比普通增量要昂贵得多),同时过度交付“大部分正确”(完全正确) .
  • 我不认为将volatile 混入其中会导致数据竞争正常,因此将num_times_done_it 的声明替换为volatile int num_times_done_it 并不能解决任何问题。
  • 有一个尴尬的解决方案,即每个线程有一个单独的计数器并将它们全部添加到report_stats,但这并不能解决doitreport_stats 之间的数据竞争。此外,它也很混乱,它假定更新是关联的,并不真正适合 Hogwild! 的用法。

是否可以在没有某种形式的同步的情况下在非平凡的多线程 C++11 程序中实现具有明确语义的调用计数器?

编辑:似乎我们可以使用memory_order_relaxed 以稍微间接的方式做到这一点:

atomic<int> num_times_done_it;
void doit() {
  num_times_done_it.store(1 + num_times_done_it.load(memory_order_relaxed),
                          memory_order_relaxed);
  // as before
}

但是,gcc 4.8.2 在 x86_64(使用 -O3)上生成此代码:

   0:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
   6:   83 c0 01                add    $0x1,%eax
   9:   89 05 00 00 00 00       mov    %eax,0x0(%rip)

并且clang 3.4 在 x86_64 上生成此代码(再次使用 -O3):

   0:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
   6:   ff c0                   inc    %eax
   8:   89 05 00 00 00 00       mov    %eax,0x0(%rip)

我对x86-TSO的理解是,这两个代码序列,除了中断和有趣的页面保护标志外,完全等同于直接生成的单指令内存inc和单指令内存add代码。 memory_order_relaxed 的这种使用是否构成数据竞争?

【问题讨论】:

  • 如果您不需要解决方案是正确的,当然可以将其优化为任意快(产生有用结果的可能性降低)。快速、正确、简单;选择两个。
  • @BenPope:这很有趣,但在这里并不是特别相关。我在问如何处理编写一个完全理论问题的时间。我所知道的任何编译器都不会为我的帖子中的模式生成不可接受的代码。但是,这可能会在未来发生变化...
  • 我不确定我是否理解每个线程有单独的计数器是多么尴尬。在doitreport_stats 之间使用单独计数器的数据竞争大部分是正确的——它对正确答案是渐近的。线程之间与doit 的数据竞争是灾难性的,最终给出了错误的答案。
  • @MarkLakata:是的。就像我说的,这完全是一个理论上的问题。我从未见过当前的编译器会破坏这一点。然而,C++ 标准说 any 数据竞争构成未定义的行为,即使是在doitreport_stats 之间的行为。关于“灾难性”,这取决于计数的使用方式! JVM 的调用次数只需近似值即可;野猪!可以证明“修复”了一个稍微错误的中间结果;原始示例中的最终用户可能只对函数被调用次数的近似值感兴趣。
  • @MarkLakata:不过,C++ 内存模型比我所知道的任何机器内存模型都弱(除了安腾); doitreport_stats 之间的数据竞赛绝对破坏了符合标准的一切。这是我在这里使用的 C++ 内存模型,而不是任何特定机器的。 (特别是,C++ 明确指出 任何 数据竞争,即使是 doitreport_stats 之间的数据竞争,也会导致未定义的行为。)

标签: c++ multithreading c++11 memory-model


【解决方案1】:

分别计算每个线程并在线程加入后总结。对于中间结果,您也可以在两者之间进行总结,但您的结果可能会关闭。这种模式也更快。您可以将它嵌入到线程的基本帮助程序类中,这样如果您经常使用它,就可以随处使用它。

而且 - 取决于编译器和平台,原子并不是那么昂贵(请参阅 Herb Sutters “原子武器”谈话 http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2),但在您的情况下,它会导致缓存出现问题,因此不建议这样做。

【讨论】:

  • 这是 OP 声明的 #3 非解决方案
  • 就是这样;在没有发生同样的数据竞争的情况下,您无法在同步某件事之前总结它们。
【解决方案2】:

看来memory_order_relaxed 技巧是正确的做法。

This blog post by Dmitry Vyukov at Intel 首先准确地回答了我的问题,然后将memory_order_relaxed storeload 列为合适的替代方案。

我仍然不确定这是否真的可以;特别是,N3710 让我怀疑我是否曾经理解过memory_order_relaxed

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多