躲不开的多线程
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,但同步是有延时的,即在一个时间内,这个内存变更对其他核来说是不可见的;而指令重排是说cpu工程师为了追求更高的性能,把不相关的指令尽可能并发执行,比如当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后对之前的变量修改是立即可见的。
但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,使拿到锁的那个线程可以很快地完成一系列流程,总体吞吐可能反而高了。
Final
多线程程序设计似乎没有一个统一的答案,pthreads,原子指令,无锁结构,但有一个可以值得肯定的就是你的设计:如何规避竞争。也可能是最重要的法则。比方说,一个依赖全局多生产者多消费者队列(MPMC)的程序不可能有很好的多核适应性,因为这个队列的极限吞吐取决于CPU的同步cache延时,lock-free或wait-free也解决不了。更好的解决方法是规避全局竞争,比如用多个SPMC或多个MPSC队列,甚至多个SPSC队列代替,在源头就规避掉竞争。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。