以下模式在许多软件中很常见,这些软件想要告诉用户它做了多少事情的次数:
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
的使用是否构成数据竞争?
答案 0 :(得分:0)
分别计算每个线程,并在线程加入后总结。对于中间结果,您也可以在两者之间进行总结,但结果可能会关闭。这种模式也更快。您可以将它嵌入到线程的基本帮助器类中,这样如果您经常使用它,就可以随意使用它。
并且 - 取决于编译器&amp;平台,原子并不昂贵(参见Herb Sutters&#34;原子武器&#34;谈论http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2)但在你的情况下它会产生缓存的问题所以它是如此不可取。
答案 1 :(得分:0)
似乎memory_order_relaxed
技巧是正确的方法。
This blog post by Dmitry Vyukov at Intel首先回答我的问题,然后列出memory_order_relaxed
store
和load
作为正确的选择。
我仍然不确定这是否真的好;特别是,N3710让我怀疑我从一开始就理解memory_order_relaxed
。