c++11标准线程库用法总结

来源:互联网 发布:淘宝规则关于假货 编辑:程序博客网 时间:2024/05/18 02:58

  • 一线程库thread
    • 通过全局函数创建线程
    • 通过伪函数创建线程
    • 通过lambda表达式创建线程
    • 通过成员函数创建线程
  • 二原子操作库atomic
  • 三互斥 mutex
    • 互斥体类
      • 1 非定时互斥体类
      • 2定时互斥锁类
    • 锁类
      • 1 简单锁 stdlock_guard
      • 2 复杂锁 stdunique_lock 
    • 多个互斥体对象上锁
    • call_once 和once_flag
    • 实例
  • 四条件变量
    • condition_variable
    • condition_variable_any
  • 五信号量类
  • 六线程异步调用和返回值
    • stdasync
    • stdfuture
    • stdshared_future
    • stdpackaged_task
  • 七this_thread
  • 八总结
  • 参考资料

一、线程库(thread)

C++11中线程较轻量级,线程的创建和执行的消耗较小,但是同时C++中线程没有明确的状态信息,如果不怕麻烦可以直接使用linux下的posix线程库,这对底层开发者还是很有用的。如果有时间我会整理一份posix 线程的相关使用手册。当然还有很多第三方优秀的c++线程库这里就不讨论了。
**c++标准线程库是c++11/14/17才支持的,很多编译器的版本还并没有完全支持他们的标准,所以在正式开始教程之前呢,先给大家提供一个c++ 最新标准的在线编译环境(http://cpp.sh/)用来测试程序。

c++11线程特点:
1. thread创建后,C++始终尝试在一个新线程中执行人物,如果失败返回std::system_error
2. 没有线程运行结果的接口,无法获得执行结果。(后面会讨论其他方法)
3. 如果有异常在thread中发生,并且没有被捕获,std::terminate将会被调用
4. 只能调用join去结束线程,或者detach去转成后台线程执行。否则当thread被析构时,程序会被abort。
5. 如果线程detach并且main函数执行结束,所以的detach线程会被终止.
6. 线程只可以通过std::move转移,不可以复制。

1. 通过全局函数创建线程

线程类的构造函数是变参构造函数,第一个参数是线程函数,后面的参数为线程
函数的参数(参数通过值传递方式,若要引用传递须加std::ref())。

include <thread>#include <iostream> void hello(int i){    std::cout << "Hello from thread :" << i<<std::endl; } int main(){    std::thread t1(hello,1);    t1.join();    return 0;}

2. 通过伪函数创建线程

//class Counter 实现 operator()1) thread   t1{Counter(1, 20)};   //c++统一推荐方法2) Counter    c(1, 20);   thread   t2(c);3) thread   t3(Counter(1,20)); #include <iostream>#include <thread>#include <chrono>using namespace std;class thread_c{public:    void operator () ()    {        cout << "hello from class member function" << endl;      }private:};int main(){thread_c  c;thread t(c);//thread t1{thread_c()}; this_thread::sleep_for(chrono::seconds(1));     return 0;}

比较第一种和第三种构造方式,如果函数对象的构造函数不需要任何参数,thread t3(Counter());是不行的,因为编译器会认为你在声明一个函数,函数名为t3,此时只能用第一种构造方式。

3. 通过lambda表达式创建线程

#include <thread>#include <iostream>#include <vector> int main(){    std::vector<std::thread> threads;    for(int i = 0; i < 5; ++i){        threads.push_back(std::thread([](){            std::cout << "Hello from thread " << std::this_thread::get_id() << std::endl;        }));    }    for(auto& thread : threads){        thread.join();    }    return 0;}

4. 通过成员函数创建线程

// class Counter c();
thread t{&Counter::process, &c};//也可以
一般常见的是一个类自己创建一个后台处理线程:thread t{&Counter::process, this};

--------------------------------------范例1------------------------------------#include <iostream>#include <thread>#include <chrono>using namespace std;class thread_c{public:    void thread_1(int a)    {        cout << "hello from class member function :" <<a<< endl;    }};int main(int argc, char **argv){    thread_c a;    thread t1(&thread_c::thread_1, a,1);    t1.join();    this_thread::sleep_for(chrono::seconds(2));    return 0;}--------------------------------------范例2------------------------------------#include <iostream>#include <thread>#include <chrono>#include <vector>using namespace std;class thread_c{public:    thread_c()    {          t1 = thread(&thread_c::thread_1,this);    };void thread_1(){    cout << "hello from class member function" << endl;}private:    thread t1;};int main(int argc, char **argv){    thread_c a;   //this_thread::sleep_for(chrono::seconds(1));    cout<<"test"<<endl;    return 0;}

线程本地存储 thread_local
thread_local int n;
n作为线程参数传递给线程,那么每个线程有一个n的副本,在线程整个生命周期中存在,且只初始化一次,如同static局部变量.

二、原子操作库(atomic)

1.实例
例如:
atomic counter(0);  //等效于 atomic_int counter(0);
…………
++counter;  //此时多个线程执行++counter是一个原子操作,是线程安全的。
例:

void   func( std::atomic<int>&   counter){  for( int   i=0;   i<1000;   ++i )    ++counter;}int main(){  std::atomic<int>   counter(0);  std::vector<std::thread>   threads;  for( int i=0;   i<10;   ++i )    threads.push_back( std::thread{ func, std::ref(counter)} );  for( auto&   t  :  threads )    t.join();      std::count<<"Result="<<counter<<std::endl;  return 0;}

三、互斥 (mutex)

编写多线程必须分外留意操作顺序,如果无法避免线程共享数据,则必须提供同
步机制,保证一次只有一个线程能更改数据。使用互斥解决竞争条件,可能导致死锁。

互斥体类

1) 非定时互斥体类

std::mutex std::recursive_mutex
方法:
  lock() : 尝试获取锁,并且阻塞直到获取锁。
  try_lock() : 尝试获取锁,并立即返回,成功获取返回true,否则false。
  unlock() : 释放锁。
mutex与recursive_mutex的区别在于,前者已经获得所后不得再尝试获取,这会死锁,后者能递归获取,注意释放次数应与获取次数相等。

2)定时互斥锁类

std::timed_mutex std::recursive_timed_mutex
方法:
  lock() , try_lock() , unlock()
  try_lock_for(rel_time) : 指定相对时间内获得返回true, 超时返回false。
  try_lock_until(abs_time) : 指定系统绝对时间内获得返回true, 超时返回false。
timed_mutex与recursive_timed_mutex区别同上。

#include <iostream>       #include <chrono>        #include <thread>         #include <mutex>          using namespace std;timed_mutex mtx;void fireworks () {    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {    cout << "-";   }   this_thread::sleep_for(std::chrono::milliseconds(1000));   cout << "*\n";   mtx.unlock();}int main (){  thread threads[10];  // spawn 10 threads:  for (int i=0; i<10; ++i)    threads[i] = thread(fireworks);  for (auto& th : threads) th.join();  return 0;}输出:------------------------------------*----------------------------------------*-----------------------------------*------------------------------*-------------------------*--------------------*---------------*----------*-----**

2. 锁类

锁类是一个包装器,析构函数会自动释放关联的互斥体。

1) 简单锁 std::lock_guard

C++中互斥锁lock之后必须调用unlock,但是考虑可能存在的复杂情况unlock
不一定会被正确调用。使用C++的RAII(Res Acquisition is init 资源获取即初
始化),利用destructor做unlock处理。其构造函数会要求获得互斥体,并阻塞直到获得锁。

如:

std::mutex mutex;...{std::lock_guard<std::mutex> lg(mutex);}//unlock when block end

尝试获取锁
std::mutex m;
while(!m.try_lock()){//可能失败即使锁可用
doSomethings();
}
std::lock_guard lg(m,std::adopt_lock);
//不会lock,但在lg对象析构时会解锁

2) 复杂锁 std::unique_lock 

   explict   unique_lock( mutex_type&   m); //阻塞直到获得锁。unique_lock(mutex_type&   m,   defer_lock_t)  noexcept; //保存一个互斥体引用,不会立即尝试获得锁。锁可以在以后获得。之后可以用std::lock() 进行加锁  unique_lock(mutex_type&   m,   try_to_lock_t); //尝试获得引用的互斥锁,未能获得也不阻塞。  unique_lock(mutex_type&   m,   adopt_lock_t); //该锁假定线程已获得引用的互斥锁,并负责管理这个锁。  template<class Clock,   class Duration>  unique_lock(mutex&  m,  const  chrono::time_point<Clock, Duration>&  abs_time); //尝试获取该锁,直到超过给定的绝对时间。  template<class Rep,   class Period>  unique_lock(mutex&  m,  const  chrono::duration<Rep, Period>&  rel_time); //尝试获取该锁,直到超过给定的相对时间。unique_lock类还支持lock(), try_lock(), try_lock_for(), try_lock_until()等方法。通过owns_lock()查看是否获得了这个锁;也可以用if对unique_lock对象直接判断是否获得锁,因为它定义了bool()运算符。

3. 多个互斥体对象上锁

std::mutex m1;std::mutex m2;...{    std::lock(m1,m2);//lock m1 and m2    //init with adopt_lock(will not do lock)    std::lock_guard<mutex>    lockM1(m1,std::adopt_lock);//只管理,不上锁    std::lock_guard<mutex> lockM2(m2,std::adopt_lock);}再例如:try多个锁(不能保证死锁处理)int idx = std::try_locK(m1,m2);if(idx<0){//成功返回 -1    lock_guard<mutex> lg1(m1,adopt_lock);    lock_guard<mutex> lg1(m1,adopt_lock);//不会lock,但保证锁可以被释放}else{    //lock failed}

参数顺序每次应保持一致, 否则易死锁。

4. call_once 和once_flag

保证call_once调度的函数只被执行一次。某些场景下,我们需要代码只被执行一次,比如单例类的初始化,考虑到多线程安全,需要进行加锁控制。C++11中提供的call_once可以很好的满足这种需求,使用又非常简单。

#include<mutex>template <class Fn, class... Args>void call_once (once_flag& flag, Fn&& fn, Args&&...args);      

第一个参数是std::once_flag的对象(once_flag是不允许修改的,其拷贝构造函数和operator=函数都声明为delete),第二个参数可调用实体,即要求只执行一次的代码,后面可变参数是其参数列表。
call_once保证函数fn只被执行一次,如果有多个线程同时执行函数fn调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于”passive execution”(被动执行状态)——不会直接返回,直到活动线程对fn调用结束才返回。对于所有调用函数fn的并发线程,数据可见性都是同步的(一致的)。
如果活动线程在执行fn时抛出异常,则会从处于”passive execution”状态的线程中挑一个线程成为活动线程继续执行fn,依此类推。一旦活动线程返回,所有”passive execution”状态的线程也返回,不会成为活动线程。(实际上once_flag相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程)。tic std::once_flag oc; // 用于call_once的局部静态变量

#include <string.h>  #include <assert.h>  #在单例模式中建立对象,最好使用call_once或者进行加锁class object  {  public:      static class object* pObject;      static object* create_new_object()      {          std::call_once(oc,[&] () { pObject = new object(); });         //不得不说c++11中的call_once真的很好用,谁用谁知道。        return pObject;      }  private:      static std::once_flag oc;    object() {}      ~object() {}  };  class object* object::pObject = NULL; int main(int argc, char* argv[])  {      object* pGlobal = object::create_new_object();      return 1;  }  

还有一个要注意的地方是 once_flag的生命周期,它必须要比使用它的线程的生命周期要长。所以通常定义成全局变量比较好。

5. 实例

// 1. 简单锁mutex   mMutex;lock_guard<mutex> mLock(mMutex);// 2. 定时锁timed_mutex   mTimeMutex;unique_lock<timed_mutex> mLock(mTimedMutex, chrono::milliseconds(200));// 3. 泛型mutex   mut1;mutex   mut2;unique_lock<mutex>  lock1(mut1, defer_lock_t());unique_lock<mutex>  lock2(mut2, defer_lock_t());lock(lock1, lock2);// 4. 双重检查锁定算法 (代替call_once的用法)class MyClass{ public:     void init() { p = new int(0);   cout<<"Init"<<endl;} private:     int *p;}MyClass  var;bool  initialized = false;mutex   mut;void  func() {    if( ! initialized)               //一次检查    {       unique_lock<mutex>  lock1(mut);      if( ! initialized)         //两次检查                 {           var.init();           initialized  = true;       }else return;    }else return;   cout<<"OK"<<endl;}//两次检查initialized。获得锁之前和获得锁之后,确保init只调用一次。

四、条件变量

1. condition_variable

只能等待unique_lock<mutex>的条件变量 notify_one();  //唤醒等待这个条件变量的线程之一 notify_all();    //唤醒所有等待这个条件变量的线程 // 1)前提是已经获得lk的锁 // 2)调用wait会unlock  lk,然后等待 // 3)当被唤醒后 lock  lk以下是重载版本,非重载版本没有predvoid wait (unique_lock<mutex>& lck, Predicate pred);wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, std::move(pred));wait_until (lck, chrono::system_clock::now() + rel_time, std::move(pred));另外,wait_for 的重载版本(predicte(2))的最后一个参数 pred 表示 wait_for 的预测条件,只有当 pred 条件为 false 时调用 wait() 才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞,因此相当于如下代码:超时返回cv_status::timeout
#include <iostream>  #include <thread>  #include <chrono>  #include <mutex>  #include <condition_variable>  using namespace std;  condition_variable cv;  mutex mtx;  void thread_1()  {      unique_lock<mutex> ulock(mtx);      cv.wait(ulock);      cout << "hello from thread_1" << endl;  }  int main(int argc, char **argv)  {      thread t1(thread_1);      this_thread::sleep_for(chrono::seconds(1));      cv.notify_one();      t1.join();      return 0;  } 

2.condition_variable_any

// condition_variable_any::wait (with predicate)#include <iostream>           // std::cout#include <thread>             // std::thread, std::this_thread::yield#include <mutex>              // std::mutex#include <condition_variable> // std::condition_variable_anystd::mutex mtx;std::condition_variable_any cv;int cargo = 0;bool shipment_available() {return cargo!=0;}void consume (int n) {  for (int i=0; i<n; ++i) {    mtx.lock();    cv.wait(mtx,shipment_available);    // consume:    std::cout << cargo << '\n';    cargo=0;    mtx.unlock();  }}int main (){  std::thread consumer_thread (consume,10);  // produce 10 items when needed:  for (int i=0; i<10; ++i) {    while (shipment_available()) std::this_thread::yield();    mtx.lock();    cargo = i+1;    cv.notify_one();    mtx.unlock();  }  consumer_thread.join();  return 0;}

五、信号量类

很遗憾c++并没有提供信号量( 信号灯)机制的库。所以可以自己设计一个,下面的程序是google的,只供参考。可以根据需求自己设计信号量类。

class CSemaphore {private:    std::condition_variable cv;    std::mutex mutex;    int value;public:    CSemaphore(int init) :            value(init) {    }    void wait() {//类似P操作        std::unique_lock<std::mutex> lock(mutex);        while (value < 1) {            cv.wait(lock);        }        value--;    }    bool try_wait() {        std::unique_lock<std::mutex> lock(mutex);        if (value < 1)            return false;        value--;        return true;    }    void post() {//类似V操作        {            std::unique_lock<std::mutex> lock(mutex);            value++;        }        cv.notify_one();    }};

六、线程异步调用和返回值

1.std::async

异步调用一个callable的对象,但是不保证调用时机。可以通过传入std::launch::async/deferred来制定是否立即调用和延迟(由系统决定调用时机)。async不保证内部使用线程池来实现,但是肯定使用一个后台线程执行任务。

2.std::future

类似于Java的future,获得任务执行结果,对于无返回值得结果使用future,如果调用future的get方法,可以保证async的会立即执行。async()方法返回值是future.
int sum(int a,int b){return a+b;}
//如果async的返回值没有被使用,那么async将一直阻塞
//编译器可以优化为 std::async(sum,1,1).get(); 阻塞调用
std::future f = std::async(sum,1,1);
int result = f.get();//获得结果,如果有异常还会抛出异常
//f.wait() 等运行完成
//f.wait_for(dur) 等待一定时间或运行完成
//f.wait_until(fp) 等待某一时刻或运行完成
//f.share() 生成一个shared_future 见下文

3.std::shared_future

类似于future,但是future的get方法只能被调用一次,二次调用可能出现未定义的行为(不一定抛出异常或出错)。shared_future的二次调用可以保证一样并且如有一次,可以保证异常一样。
std::shared_future f = std::async(sum,1,1).share();//需要使用share来获得。

4.std::packaged_task

类似于async,但是可以自己控制调用时机,也可以使用线程去调用任务执行。std::packaged_task<int(int,int)> task(sum);//封装函数std::future<int> f = task.get_future();//获得futuretask(1,1); //直接调用//std::thread t{std::move(task),5,5}; //线程方式调用//t.join();//need join or detach threadint result = f.get();//获得结果/////////////////////////////////////////////////////////////////////////// packaged_task example#include <iostream>     // std::cout#include <future>       // std::packaged_task, std::future#include <chrono>       // std::chrono::seconds#include <thread>       // std::thread, std::this_thread::sleep_for// count down taking a second for each value:int countdown (int from, int to) {  for (int i=from; i!=to; --i) {    std::cout << i << '\n';    std::this_thread::sleep_for(std::chrono::seconds(1));  }  std::cout << "Lift off!\n";  return from-to;}int main (){  std::packaged_task<int(int,int)> tsk (countdown);   // set up packaged_task  std::future<int> ret = tsk.get_future();            // get future  std::thread th (std::move(tsk),10,0);   // spawn thread to count down from 10 to 0  // ...  int value = ret.get();                  // wait for the task to finish and get result  std::cout << "The countdown lasted for " << value << " seconds.\n";  th.join();  return 0;}

七、this_thread

this_thread::get_id: 返回当前线程的id
this_thread::yield: 让调度器先运行其它的线程,这在忙于等待状态时很有用
this_thread::sleep_for: 将当前线程置于阻塞状态,时间不少于参数所指定的时间段
this_thread::sleep_util: 在指定的时刻来临前,一直将当前的线程置于阻塞状态
总结:
std::this_thread::yield() 的目的是避免一个线程(that should be used in a case where you are in a busy waiting state)频繁与其他线程争抢CPU时间片, 从而导致多线程处理性能下降.
std::this_thread::yield() 是让当前线程让渡出自己的CPU时间片(给其他线程使用)
std::this_thread::sleep_for() 是让当前休眠”指定的一段”时间.
sleep_for()也可以起到 std::this_thread::yield()相似的作用, (即:当前线程在休眠期间, 自然不会与其他线程争抢CPU时间片)但两者的使用目的是大不相同的: std::this_thread::yield() 是让线程让渡出自己的CPU时间片(给其他线程使用) sleep_for() 是线程根据某种需要, 需要等待若干时间.

八、总结

啊,感觉像长篇大论了一波,看到这里的童鞋说明还是很有耐心的哟。整理这份资料可花了我不少时间,之前学的时候并没有什么感觉,现在整理出来后发现知识点还是蛮多的哈。
上面的程序部分是来至于网络,仅供参考,只是为了说明用法并无实际意义。如果对锁机制还有不懂的地方,在我的下一篇博客中会展开说明。整理了这么多只为了让学c/c++的同学不孤单(c++的处境已经不如10年之前了,除了学校开课以外,学的人越来越少了),给你一个更方便的学习参考。让我们记住这门古老的语言,因为它的强大只有我们自己知道。以后我还会陆续推出更多总结和小案例。文中存在缺陷或不足之处在所难免,还望大佬们多多指教。

参考资料

c++ reference
网络资料整理