1. 程式人生 > >C++11多執行緒基本使用

C++11多執行緒基本使用

C++11增加了執行緒及執行緒相關的累,很方便的支援了併發程式設計,使得編寫的多執行緒程式的可移植性得到了很大的提高.

執行緒的建立

用std::thread 建立執行緒非常的簡單,只需要提供執行緒函式或者函式物件即可,並可以同時指定執行緒的引數:

#include<iostream>
#include<thread>
#include<chrono>
using namespace std;

//執行緒函式
void func(int a, int b, int c)
{
    std::this_thread::sleep_for(std::chrono::seconds(3
)); cout << a << " " << b << " " << c << endl; } int main() { //建立執行緒物件t1,繫結執行緒函式為func std::thread t1(func, 1, 2, 3); //輸出t1的執行緒ID std::cout << "ID:" << t1.get_id() << std::endl; //等待t1執行緒函式執行結束 t1.join(); std::thread t2(func, 2
, 3, 4); //後臺執行t2的執行緒函式,並且不會因為main函式結束時,執行緒函式未執行完而產生異常 t2.detach(); cout << "after t2 ,main is runing" << endl; //以lambda表示式作為被幫頂的執行緒函式 std::thread t4([](int a, int b, int c) { //執行緒休眠5秒 std::this_thread::sleep_for(std::chrono::seconds(5)); cout
<< a << " " << b << " " << c << endl; }, 4,5,6); t4.join(); //獲取CPU的核數 cout << "CPU: " << thread::hardware_concurrency() << endl; //當新增下面註釋掉的語句會丟擲異常,因為執行緒物件先於執行緒函式結束了,應該保證執行緒物件的生命週期線上程函式執行完時仍然存在. //std::thread t3(func, 3, 4, 5); return 0; }
  • 執行緒函式將會運行於執行緒物件t中,join函式將會阻塞執行緒,直到執行緒函式執行結束,如果執行緒函式有返回值,返回值將被忽略.

  • detach可以將執行緒與執行緒物件分離,讓執行緒作為後臺執行緒執行,當前執行緒也不會阻塞了.但是detach之後就無法在和執行緒發生聯絡了.如果執行緒執行函式使用了臨時變數可能會出現問,執行緒呼叫了detach在後臺執行,臨時變數可能已經銷燬,那麼執行緒會訪問已經被銷燬的變數.join能保證.

  • 雖然這種方式建立執行緒很方便,但是std::thread 出了作用域後將會析構,這個時候執行緒函式還沒執行完則會發生錯誤.

  • 執行緒不可以複製但是可以移動.但是執行緒移動後,執行緒物件將不再代表任何執行緒了:

        std::thread t(func);
        //移動後,執行緒物件t不在代表任何執行緒
        std::thread t1(std::move(t));
        // t.join();
        t1.join();

互斥量

互斥量是一種同步原語,是一種執行緒同步的手段,用來保護多執行緒同時訪問的共享資料.

  • std::mutex: 獨佔的互斥量,不能遞迴使用.

  • std::timed_mutex: 帶超時的獨佔互斥量,不能遞迴使用.

  • std::recursive_mutex: 遞迴互斥量,不帶超時功能.

  • std::recursive_timed_mutex: 帶超時的遞迴互斥量.

這些互斥量的基本介面十分相近,都是通過lock()來阻塞執行緒,直到獲得互斥量的所有權為止.線上程或的互斥量並完成任務後,就必須使用unlock()來解除對互斥量的佔用,lock和unlock必須成對出現.try_lock()嘗試鎖定互斥量,成功返回true,失敗返回false,他是非阻塞的.

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

std::mutex g_lock;

void func()
{
    //上鎖
    g_lock.lock();
    cout << "in id: " << this_thread::get_id() << endl;
    this_thread::sleep_for(chrono::seconds(1));
    cout << "out id: " << this_thread::get_id() << endl;
    //解鎖
    g_lock.unlock();
}

void f()
{
    //lock_guard在構造時會自動鎖定互斥量,而在退出作用域後進行析構時就會自動解鎖.
    lock_guard<std::mutex> lock(g_lock);
    cout << "in id: " << this_thread::get_id() << endl;
    this_thread::sleep_for(chrono::seconds(1));
    cout << "out id: " << this_thread::get_id() << endl;
}

int main()
{
    std::thread t1(func);
    std::thread t2(func);
    std::thread t3(func);

    t1.join();
    t2.join();
    t3.join();

    std::thread t4(f);
    std::thread t5(f);
    std::thread t6(f);

    t4.join();
    t5.join();
    t6.join();
}
  • lock_guard用到了RAII的技術,這種技術在類的建構函式中分配資源,在解構函式中釋放資源,保證資源在出了作用域之後就釋放.

  • std::recursive_mutex遞迴鎖允許同一個執行緒多次獲得互斥量.但是儘量不要使用遞迴鎖:

    1. 需要用到遞迴鎖定的多執行緒互斥處理往往本身就是可以簡化的,允許遞迴互很容易放縱複雜邏輯的產生,從而導致一些多執行緒同步引起的晦澀問題.
    2. 遞迴鎖比起非遞迴鎖,效率會低.
    3. 遞迴鎖雖然允許同一個執行緒多次獲得同一互斥量,但是可重複獲得的最大次數並未具體說明,一旦超過一定次數就會丟擲異常.
  • 帶超時的互斥量在獲取鎖的時候增加了超時等待功能,因為有時不知道獲取鎖需要多久,為了不至於一直等待獲取互斥量,就設定一個等待超時時間,在超時後還可以做其他的的事情.

#include<iostream>
#include<thread>
#include<mutex>
#include<chrono>
using namespace std;

std::timed_mutex mutex1;

void work()
{
    //設定阻塞時間
    std::chrono::milliseconds timeout(100);

    while (true) {
        //帶超時的鎖,當阻塞超過100milliseconds時返回false
        if (mutex1.try_lock_for(timeout)) {
            cout << this_thread::get_id() << ": do work with the mutex" << endl;
            std::chrono::milliseconds sleepDuration(250);
            this_thread::sleep_for(sleepDuration);
        } else {
            cout << this_thread::get_id() << ": do work without mutex" << endl;

            chrono::milliseconds sleepDuration(100);
            std::this_thread::sleep_for(sleepDuration);
        }
    }
}

int main()
{
    std::thread t1(work);
    std::thread t2(work);

    t1.join();
    t2.join();

    return 0;
}

條件變數

  • 條件變數阻塞一個或多個執行緒,直到收到另外一個執行緒發來的通知或者超時,才會喚醒當前阻塞的程序,條件變數需要和互斥量配合使用.

  • C++11提供了兩種條件變數

    1. std::condition_variable,配合std::unique_lock進行wait操作
    2. std::condition_variable_any,和任意帶有lock,unlock的mutex進行搭配使用,比較靈活但效率略低。
  • 條件變數的使用過程如下:

    1. 擁有條件變數的執行緒獲取互斥鎖
    2. 迴圈檢查某個條件,如果條件不滿足,則阻塞直到條件滿足,如果條件滿足,則向下執行.
    3. 某個執行緒滿足條件執行完之後呼叫notify_one或notify_all喚醒一個或者所有的等待執行緒.

eg:

//同步佇列的實現
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<list>
using namespace std;

template <typename T>
class SyncQueue
{
private:
    //資料緩衝區
    std::list<T> m_queue;
    //互斥鎖
    std::mutex m_mutex;
    //不為滿的條件變數
    std::condition_variable_any m_notFull;
    //不為空的條件變數
    std::condition_variable_any m_notEmpty;
    //緩衝區最大大小
    int m_maxsize;

    //判斷是否為滿,因為給內部成員函式使用,而這些函式在呼叫前都已經上過鎖了,所以無需在加鎖
    bool IsFull()
    {
        return m_queue.size() == m_maxsize;
    }

    //判斷是否為空
    bool IsEmpty()
    {
        return m_queue.empty();
    }
public:
    SyncQueue(int max):m_maxsize(max) {  }
    //相緩衝區新增資料
    void Put(const T& x)
    {
        //unique_lock與lock_guard相似,但是後者只能在析構時才釋放鎖,而前者可以隨時釋放鎖
        std::unique_guard<std::mutex> locker(m_mutex);

        //若為滿則需等待,而不能相緩衝區中新增
        while (IsFull())
        {
            std::cout << "data Full" << std::endl;
            //若為滿,訊號變數進行阻塞等待,此時釋放m_mutex鎖,然後直到被notify_one或者notify_all喚醒後先獲取m_mutex鎖
            m_notFull.wait(m_mutex);
        }
        //相緩衝區新增資料
        m_queue.push_back(x);
        //喚醒處於等待中的非空條件變數
        m_notEmpty.notify_one();
    }

    //從緩衝區獲取資料
    void Take(T& x)
    {
        std:unique_guard<std::mutex> locker(m_mutex);

        //直接使用這種方法,就無需在定義私有的Empty,也無需寫while迴圈判斷了.
        //m_notEmpty.wait(locker, [this] {return !m_queue.empty();});

        //若為空則需等待,而不能從緩衝區中取出
        while(IsEmpty())
        {
            std::cout << "data Empty" << std::endl;
            m_notEmpty.wait(m_mutex);
        }
        //獲取資料
        x = m_queue.front();
        //刪除被獲取的資料
        m_queue.pop_front();
        m_notFull.notify_one();
    }

    bool Empty()
    {
        std::lock_guard<std::mutex> locker(m_mutex);
        return m_queue.empty();
    }

    bool Full()
    {
        std::lock_guard<std::mutex> locker(m_mutex);
        return m_queue.size() == m_maxsize;
    }

    size_t Size()
    {
        std::lock_guard<std::mutex> locker(m_mutex);
        return m_queue.size();
    }
};

原子變數

  • C++11提供了一個原子型別std::atomic<T>,可以使用任意型別作為模板引數,C++11內建了整性的原子變數,使用原子變數就不需要使用互斥量來保護改變量了.
#include<atomic>

struct AtomicCounter {
    std::atomic<int> value;

    void increment()
    {
        ++ value;
    }

    void decrement()
    {
        -- value;
    }

    int get()
    {
        return value;
    }
};

call_once/once_flag

  • 為了保證在多執行緒環境中某個函式僅被呼叫一次,例如,需要初始化某個物件,而這個物件智慧被初始化一次的話,就可以使用std::call_once來保證函式在多執行緒環境下只調用一次.
#include<iostream>
#include<trhead>
#include<mutex>

std:once_flag flag;

void do_once()
{
    std::call_once(flag, []() {std::cout << "called" << std::endl;});
}

int main()
{
    std::thread t1(do_once);
    std::thread t2(do_once);
    std::thread t3(do_once);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}