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