• 躲不开的多线程


    本文为原创,转载请注明: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-freewait-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 conditionABA problem,完成相同目的的代码比用mutex更复杂。
    • 出现竞争时mutex会使调用者睡眠,在高度竞争时避免了频繁的cache bouncing,使拿到锁的那个线程可以很快地完成一系列流程,总体吞吐可能反而高了。

    因为non-mutex和mutex可以完成相同的功能和接口,这里有个简单有效原则:使用基准测试进行性能对比,确认更适合你业务的数据结构。

    Final

    多线程程序设计似乎没有一个统一的答案:pthreads,原子指令,无锁结构,都不是放之四海而皆准的办法。但有一个可以肯定的就是你的设计:如何规避竞争。比方说,一个依赖全局多生产者多消费者队列(MPMC)的程序不可能有很好的多核适应性,因为这个队列的极限吞吐取决于CPU的同步cache延时,lock-free和wait-free也解决不了。更好的解决方法是规避全局竞争,比如用多个SPMC或多个MPSC队列,甚至多个SPSC队列代替,在源头就规避掉竞争。也许这就是最重要的法则。

  • 相关阅读:
    如何快速正确的安装 Ruby, Rails 运行环境
    五种开源协议的比较(BSD,Apache,GPL,LGPL,MIT) – 整理
    CocoaPods安装和使用教程
    ARC下需要注意的内存管理
    iOS 遍历某一对象的属性和方法
    使用命令行工具运行Xcode 7 UI Tests
    手势知多少
    Customizing UIWebView requests with NSURLProtocol
    iOS: JS和Native交互的两种方法
    NSURLProtocol
  • 原文地址:https://www.cnblogs.com/gistao/p/4463934.html
Copyright © 2020-2023  润新知