首先,您必须学会像语言律师一样思考。
C ++规范未引用任何特定的编译器,操作系统或CPU。它引用了抽象机,它是对实际系统的概括。在语言律师界,程序员的工作是为抽象机编写代码。编译器的工作是在具体机器上实现该代码。通过严格按照规范进行编码,可以确定您的代码可以在不使用兼容C ++编译器的任何系统上进行编译和运行,而无论是现在还是50年后。
C ++ 98 / C ++ 03规范中的抽象机基本上是单线程的。因此,不可能编写相对于规范“完全可移植”的多线程C ++代码。规范甚至没有说关于内存加载和存储的原子性或加载和存储发生的顺序的任何事情,不用管互斥锁之类的事情。
当然,您可以在实践中为特定的具体系统(例如pthread或Windows)编写多线程代码。但是,没有为C ++ 98 / C ++ 03编写多线程代码的标准方法。
C ++ 11中的抽象机在设计上是多线程的。它还具有定义明确的内存模型;也就是说,它说明了编译器在访问内存时可能会做或可能不会做的事情。
考虑以下示例,其中两个线程同时访问一对全局变量:
Global
int x, y;
Thread 1 Thread 2
x = 17; cout << y << " ";
y = 37; cout << x << endl;
线程2可能输出什么?
在C ++ 98 / C ++ 03下,这甚至不是“未定义行为”;这个问题本身是没有意义的,因为该标准并未考虑任何称为“线程”的内容。
在C ++ 11下,结果是未定义行为,因为加载和存储通常不需要是原子的。看起来似乎并没有太大的改善...就其本身而言,不是。
但是,使用C ++ 11,您可以编写以下代码:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17); cout << y.load() << " ";
y.store(37); cout << x.load() << endl;
现在事情变得更加有趣了。首先,在这里定义行为。现在可以打印线程2 0 0
(如果它在线程1之前运行),37 17
(如果它在线程1之后运行)或0 17
(如果在线程1分配给x之后但又分配给y之后运行)。
它不能打印的是37 0
,因为C ++ 11中原子加载/存储的默认模式是强制顺序一致性。这只是意味着所有加载和存储必须“好像”它们按照您在每个线程中写入它们的顺序进行,而线程之间的操作可以交错,但是系统喜欢。所以原子能的默认行为,同时提供了原子和排序的加载和存储。
现在,在现代CPU上,确保顺序一致性可能很昂贵。特别是,编译器很可能在每次访问之间发出完全成熟的内存屏障。但是,如果您的算法可以容忍乱序的加载和存储;即,如果它需要原子性但不需要排序;即,如果它可以容忍37 0
此程序的输出,则可以编写以下代码:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
CPU越现代,它比上一个示例更快的可能性就越大。
最后,如果只需要按顺序保留特定的装入和存储,则可以编写:
Global
atomic<int> x, y;
Thread 1 Thread 2
x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
这将我们带回到有序的装载和存储- 37 0
不再是可能的输出-却以最小的开销做到了。(在这个简单的示例中,结果与成熟的顺序一致性相同;在较大的程序中,结果则不是。)
当然,如果要查看的唯一输出是0 0
或37 17
,则只需在原始代码周围包裹一个互斥体即可。但是,如果您已经读了那么多书,我敢打赌,您已经知道它是如何工作的,并且这个答案已经比我打算的要长:-)。
因此,底线。互斥体很棒,C ++ 11对其进行了标准化。但是有时出于性能原因,您需要较低级别的基元(例如,经典的双重检查锁定模式)。新标准提供了诸如互斥锁和条件变量之类的高级小工具,还提供了诸如原子类型和各种不同的内存屏障之类的低级小工具。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且可以确定您的代码可以在当今和未来的系统上编译并运行不变。
坦率地说,除非您是专家并且致力于一些严肃的低级代码,否则您应该坚持使用互斥锁和条件变量。那就是我打算做的。
有关这些内容的更多信息,请参见此博客文章。