以下模式在许多想要告诉用户它做了多少次各种事情的软件中很常见:
int num_times_done_it; // global
void doit() {
++num_times_done_it;
// do something
}
void report_stats() {
printf("called doit %i timesn", num_times_done_it);
// and probably some other stuff too
}
不幸的是,如果多个线程可以在没有某种同步的情况下调用doit
,那么对num_times_done_it
的并发读取-修改-写入可能是一场数据竞赛,因此整个程序的行为将是未定义的。此外,如果report_stats
可以在没有任何同步的情况下与doit
同时调用,则在修改num_times_done_it
的线程和报告其值的线程之间存在另一个数据竞争。
通常,程序员只想用尽可能少的开销来正确地计算调用doit
的次数。
(如果你认为这个例子微不足道,那么使用这个技巧,Hogwood!与无数据竞争的随机梯度下降相比,获得了显著的速度优势。此外,我相信Hotspot JVM正是对方法调用计数的共享计数器进行这种无保护的多线程访问——尽管这是显而易见的,因为它生成的是汇编代码,而不是C++11。)
明显无解决方案:
- 原子,根据我所知的任何内存顺序,在这里都会"尽可能少的开销"失败(原子增量可能比普通增量贵得多),同时过度交付"基本正确"(通过完全正确)
- 我不认为将
volatile
放入混合中会导致数据竞赛,所以用volatile int num_times_done_it
替换num_times_done_it
的声明并不能解决任何问题 - 有一种尴尬的解决方案,即每个线程有一个单独的计数器,并将它们全部添加到
report_stats
中,但这并不能解决doit
和report_stats
之间的数据竞争。此外,这很混乱,它假设更新是关联的,并不真正适合Hogward!"s的用法
在没有某种形式的同步的情况下,在一个非平凡的多线程C++11程序中,是否可以实现具有良好语义的调用计数器?
EDIT:似乎我们可以使用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
的使用是否构成数据竞赛?
分别为每个线程计数,并在线程连接后相加。对于中间结果,您也可以在两者之间进行总结,但您的结果可能是不正确的。这种模式也更快。您可以将它嵌入到线程的基本助手类中,这样,如果您经常使用它,您就可以随时随地使用它。
并且-取决于编译器&平台,原子技术并没有那么昂贵(参见Herb Sutters的"原子武器"演讲http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Start-atomic-Weapons-1/2),但在您的情况下,这会给缓存带来问题,因此不可取。
似乎memory_order_relaxed
技巧是正确的方法。
这篇由英特尔的Dmitry Vyukov撰写的博客文章一开始就准确地回答了我的问题,并将memory_order_relaxed
、store
和load
列为合适的替代品。
我仍然不确定这是否真的可以;特别是,N3710让我怀疑自己当初是否理解过memory_order_relaxed
。