本文为原创,转载请注明:http://www.cnblogs.com/gistao/
Background
先来看一段程序。
线程1
ready = false; init(p); ready = true; 线程2 if (ready) { p.bar(); }
线程2当ready为true时才会访问p,而在线程1里如果ready为true的话说明p已经初始化好了,这样子看起来肯定是没问题的,可是对于cpu来说,这段代码几乎是不能正确执行的。关键在于内存可见性(visibility)和指令重排(reordering)。在SMP架构下,每一个核都有自己的cache(一般是L1和L2),当不同的核修改同一段内存时,cpu会同步不同核的cache,这个叫cache一致性。但同步是有延时的,即在一个时间内,这个内存变更对其他核来说是不可见的。而指令重排是说cpu工程师为了追求更高的性能,把不相关的指令尽可能的并发执行,比如当前指令是从内存里copy数据,这个时间相对来说是比较久的,cpu不会等copy完成,会紧接着执行下一条无相关指令。针对这段代码来说,read=true这条指令有可能会重排到init方法完成之前(指令不相关),线程2在看到ready变为true后就对p访问就会出错,假如即使并没有重排,当线程2看到ready变为true之后,也很有可能因为visibility导致它没有看到p的变化,继而导致访问出错。
Pthreads
实现POSIX线程标准的库被称作Pthreads,为了解决各线程同时访问一段内存,库提供了原子访问和内存可见性两大保证,这里举一个可见性原则:当线程A修改变量后调用pthread_unlock mutex,且线程B成功pthread_lock mutex后,对线程A之前的变量修改是立即可见的。
但mutex往往也会造成性能过低,当临界区过大会限制线程的并发,而临界区过小会造成上下文的频繁切换(此时应考虑adaptive mutex)。而随着硬件的发展,并行编程变得越来越重要。
Atomic Instructions
原子指令是并行编程的基石,c++11正式引入了原子指令。
原子指令 (x为std::atomic<int>)
|
作用
|
---|---|
x.store(n) | 把x设为n |
x.exchange(n) | 把x设为n并返回设定之前的值 |
x.compare_exchange_weak(expected_ref, desired) | 相比strong版本,可能有spurious wakeup |
x.fetch_add(n), x.fetch_sub(n), x.fetch_xxx(n) | 给x加/减上n(或更多指令),返回修改之前的值 |
x.compare_exchange_strong(expected_ref, desired) |
若x等于expected_ref,则设为desired,否则把x值写入expected_ref。返回是否成功 |
x.load() | 返回x的值 |
原子指令不是mutex,为了解决重排问题,stl封装了memory order。
memory order
|
作用
|
---|---|
memory_order_acquire | 防止后面的访存指令重排至此条指令之前 |
memory_order_consume | 防止后面依赖此原子变量的访存指令重排至此条指令之前 |
memory_order_release | 防止前面的访存指令重排至此条指令之后,当此次指令的结果对其他线程可见后,之前的所有指令都可见 |
memory_order_relaxed | 没有fencing作用 |
memory_order_acq_rel | acquire + release语意 |
memory_order_seq_cst | acq_rel语意外加所有使用seq_cst的指令有严格地全序关系 |
更重要的是,原子指令赋予了我们无锁数据结构(Non-Blocking Data Structures):lock-free和wait-free。
Non-Blocking Data Structures
boost1.53版本提供了几个lock-free数据结构
boost::lockfree::queue
boost::lockfree::stack
boost::lockfree::spsc_queue
详见这篇翻译。
值得说的是,lock-free或wait-free听起来比mutex高大上的多,其实有时效果反而会慢,因为
- lock-free和wait-free需要处理复杂的race condition和ABA problem,完成相同目的的代码比用mutex更复杂。
- 出现竞争时mutex会使调用者睡眠,在高度竞争时避免了频繁的cache bouncing,使拿到锁的那个线程可以很快地完成一系列流程,总体吞吐可能反而高了。
因为non-mutex和mutex可以完成相同的功能和接口,这里有个简单有效原则:使用基准测试进行性能对比,确认更适合你业务的数据结构。
Final
多线程程序设计似乎没有一个统一的答案:pthreads,原子指令,无锁结构,都不是放之四海而皆准的办法。但有一个可以肯定的就是你的设计:如何规避竞争。比方说,一个依赖全局多生产者多消费者队列(MPMC)的程序不可能有很好的多核适应性,因为这个队列的极限吞吐取决于CPU的同步cache延时,lock-free和wait-free也解决不了。更好的解决方法是规避全局竞争,比如用多个SPMC或多个MPSC队列,甚至多个SPSC队列代替,在源头就规避掉竞争。也许这就是最重要的法则。