c++常用多线程知识

多线程主题本身是个大课题,简单起见,将c++多线程编程的挑战归结于对资源的同步访问。这里的资源可以指单个变量,一个类,或者说一个线程产生的东西另外一个线程去消费。对此,c++缺乏语言层面的支持。下面介绍下常见的同步访问类型。

1. 整型值的同步访问,经典的例子就是递增或者递减变量,比如++i 等于是i=i+1,对i递增有如下几个步骤:从内存中取值,将该值加1,最后将新值保存到i的内存位置,这个过程成为"读-改-写"(RMW,Read-Modify-Write), 很显然该过程非原子的,是可以被其他线程中断的。各操作系统提供了线程安全的操作方式,这种做法可以节约可观的性能开销。

以下为一个简单的原子类型包装,原子类型常用于多线程引用计数。

        class AtomicNumber
        {
        public:
                AtomicNumber(){mValue=0;}
                long Get(){return mValue;}
                long Set(int n){return __sync_lock_test_and_set(&mValue,n);}
                long Inc(){return __sync_add_and_fetch(&mValue,1);}
                long Dec(){return __sync_sub_and_fetch(&mValue,1);}
                long Add(int n){return __sync_add_and_fetch(&mValue,n);}
                long Sub(int n){return __sync_sub_and_fetch(&mValue,n);}
    		bool Cas(long cmp,long newv) {return __sync_bool_compare_and_swap(&mValue,cmp,newv);}
        private:
                volatile long mValue;
        };


2. 对代码块的同步访问:临界区,对大多数同步需求来说,单个原子操作不够,他们往往需要对某个代码块独占的访问,这也是我们最广泛使用的同步方式,也是相对来说代价高昂的一种方式,但是这种方式使用简单,不容易出错。这种方式往往会伴随内核切换,这种切换代价高昂。下面介绍一种特殊的基于轮询的一种互斥体,简单来说轮询就是不断反复测试来等待某个条件的改变,称之为自旋互斥体,轮询意味着会吞噬cpu周期。但是某些场合还是非常适合使用这种自旋互斥体的,比如临界区非常短小,资源竞争也不那么激烈的时候,这种方式就很适合。下面给出一个来自于facebook开源的超轻量的自旋锁,该实现的性能调优点就是先轮询等待最多4000次,超过4000次则主动交出cpu,防止竞争激烈的时候大量占用cpu。

 class Sleeper 
  {
    static const unsigned kMaxActiveSpin = 4000;
    unsigned spinCount;
  public:
    Sleeper() : spinCount(0) {}
    void wait() {
      if (spinCount < kMaxActiveSpin) 
      {
        ++spinCount;
        asm volatile("pause");
      } else 
      {
        struct timespec ts = { 0, 500000 };
        nanosleep(&ts, NULL);
      }
    }
  };

  class SpinLock
  {
  public:
    SpinLock(){lock_.Set(FREE);}
    void Lock()
    {
      Sleeper sleeper;
      do 
      {
        sleeper.wait();
      } while(!lock_.Cas(FREE,LOCKED));
    }
    void Unlock()
    {
      lock_.Set(FREE);
    }
  private:
    enum { FREE = 0, LOCKED = 1 };
    AtomicNumber lock_;
  };

3:在线程间传递消息真心烦人,一步做错,数据就会损坏,有两种方式处理这个问题,一种就是简单的方式,一种是麻烦的方式。

简单的方式:使用你使用平台提供的锁(互斥、临界区域,或等效)。这从概念上不难理解,使用上更简单。你无需担心排列问题,库或OS会替你解决。使用锁的唯一问题就是慢(这里的“慢”是相对而言的:一般应用,它的速度足够了)。

困难的方式:使用无锁的方式,为什么不是每个人都使用无锁编程呢?嗯,它不那么容易用对。任何情况下,不管每个线程在做什么,你必须很小心,绝不能破坏数据。但更难的是顺序相关的保证。在多核环境,代码的执行顺序可能并不是我们表面看到编码顺序,因为编译器和cpu会进行各种优化。

考虑这个例子(a和b从0开始):

Thread A    Thread B
b = 42
a = 1
            print(a)
            print(b)

线程B可能会输出的一组值是什么呢?有可能是{0 0 },{1 42},{0 42}和{1 0}, {0 0}, {1 42}中的任何一组,这里有两种情况会导致乱序,一是编译器优化乱序,二是cpu执行乱序,为了保障这种顺序一致性就必须了解有什么手段可以保障,答案就是内存屏障

内存屏障:也就是让一个CPU处理单元中的内存状态对其它处理单元可见的一项技术,在不同的CPU架构上内存屏障的实现非常不一样,大多数的内存屏障都是复杂的话题,我们可以简单理解为一种保障代码执行顺序的手段。

分享两个实现,一个是单一生产者单一消费者队列,一个是多生产者多消费者队列

lock-free 的消息队列,这是网络程序中经常用到的方式,实现这些队列的核心就是 atomic, 其中需要注意的就是内存访问的顺序控制,解决伪共享(false sharing)以及无锁算法。


4:linux 2.6.22以后的内核版本中增加了eventfd,可以用来实现线程间的等待通知机制,eventfd使用的仍然是“一切皆文件”的思想,创建好的eventfd可以通过epoll来监听读写事件,结合消息队列,可以实现高效的线程间通讯。比如线程A准备好了数据,作为生产者push进队列,通过eventfd通知线程B,线程B监听到读事件后,会被唤醒,线程B作为消费者从消息队里中pop消息


5:根据需求选择合适的同步或者通讯方式






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值