1. 程式人生 > >使用C++11進行多執行緒開發

使用C++11進行多執行緒開發

建立執行緒

C++11 增加了執行緒以及執行緒相關的類, 而之前並沒有對併發程式設計提供語言級別的支援

std::thread 類

使用 std::thread 類來建立執行緒, 我們需要提供的只是執行緒函式, 或者執行緒物件, 同時提供必要的引數
std::thread 表示單個執行的執行緒, 使用thread 類首先會構造一個執行緒物件, 然後開始執行執行緒函式,

#include <iostream>
#include <thread> //需要包含的頭

using namespace std;

void func(int a, double b)  //有引數, 引數數量不限
{
    cout << a << ' ' << b << endl;
}

void func2() //無引數
{
    cout << "hello!\n";
}

int main() 
{
    thread t1(func, 1, 2); //提供引數
    thread t2(func2);

    //可以使用 lambda表示式
    thread t3([](int a, double b){cout << a << ' ' << b << endl;}, 3, 4);

    cout << t1.get_id()  << "****" << endl;  //可以使用 get_id() 獲取執行緒 id
    t1.join();
    t2.join();
    t3.join();

    return 0;
}

使用join()

我們知道, 上例中如果主執行緒 (main) 先退出, 那些還未完成任務的執行緒將得不到執行機會, 因為 main 會在執行完呼叫 exit(), 然後整個程序就結束了, 那它的"子執行緒" (我們知道執行緒是平級的, 這裡只是, 形象一點) 自然也就 over 了
所以就像上例中, 執行緒物件呼叫 join() 函式, join() 會阻塞當前執行緒, 直到執行緒函式執行結束, 如果執行緒有返回值, 會被忽略

使用 detach()

對比於 join(), 我們肯定有不想阻塞當前執行緒的時候, 這時可以呼叫 detach(), 這個函式會分離執行緒物件和執行緒函式, 讓執行緒作為後臺執行緒去執行, 當前執行緒也不會被阻塞了, 但是分離之後, 也不能再和執行緒發生聯絡了, 例如不能再呼叫 get_id()

來獲取執行緒 id 了, 或者呼叫 join() 都是不行的, 同時也無法控制執行緒何時結束

#include <thread>
void func() 
{
	//...
}

int main() 
{
	std::thread t(func);
	t.detach();
	// 可以做其他事了, 並不會被阻塞
	return 0;
}

程式終止後, 不會等待在後臺執行的其餘分離執行緒, 而是將他們掛起, 並且本地物件被破壞

警惕作用域

std::thread 出了作用域之後就會被析構, 這時如果執行緒函式還沒有執行完就會發生錯誤, 因此, 要注意保證執行緒函式的生命週期線上程變數 std::thread

之內

執行緒不能複製

std::thread 不能複製, 但是可以移動
也就是說, 不能對執行緒進行復制構造, 複製賦值, 但是可以移動構造, 移動賦值

#include <iostream>
#include <thread>

void func() 
{
    std::cout << "here is func" << std::endl;
}

int main() 
{
    std::thread t1(func);
    std::thread t2;
    t2 = t1; //error

    t2 = std::move(t1); //right, 將 t1 的執行緒控制權轉移給 t2
    
    std::cout << t1.get_id() << std::endl;  //error,t1已經失去了執行緒控制權

    t1 = std::thread(func); //right, 直接構造, 建立的是臨時物件,所以隱式呼叫move

    t1 = std::move(t2); //error, 不能通過賦值一個新值來放棄一個已有執行緒, 這樣會直接導致程式崩潰
}

std::thread= 過載了, 呼叫 operator= 是移動建構函式, 複製被禁用了,

給執行緒傳參

傳遞指標

#include <iostream>
#include <thread>

void func(int* a)
{
    *a += 10;
}

int main()
{
    int x = 10;
    std::thread t1(func, &x);
    t1.join();
    std::cout << x << std::endl;

    return 0;
}

上例程式碼, 可以如願改變 x 的值, 但是看下面的程式碼, 當我們傳遞引用時, 卻好像並不能如我們所想

傳遞引用

#include <iostream>
#include <thread>

void func(int& a)
{
    a += 10;
}

int main()
{
    int x = 10;
    std::thread t1(func, x);
    t1.join();
    std::cout << x << std::endl;

    return 0;
}

我們想讓 func 函式對 x 進行更新, 但是實際上給執行緒傳參會以拷貝的形式複製到執行緒空間, 所以即使是引用, 引用的實際上是新執行緒堆疊中的臨時值, 為了解決這個問題, 我們需要使用引用包裝器 std::ref()
改成:
std::thread t1(func, std::ref(x));

實際上, 我的編譯器對於這段程式碼直接給出了編譯錯誤…

以類成員函式為執行緒函式

因為類內成員涉及 this 指標, 就和所需的執行緒函式引數不同了

#include <iostream>
#include <thread>

using namespace std;

class A 
{
public:
    void func1()  
    {
        cout << "here is class A`s func 1" << endl;
    }
    static void func2() 
    {
        cout << "here is class A`s func 2" << endl;
    }

    void func3() 
    {
        thread t1(&A::func1, this);	//非靜態成員函式
        thread t2(A::func2);		//靜態成員函式
        
        t1.join();
        t2.join();
    }
};


int main()
{
    A a;
    thread t1(&A::func1, &a);	//非靜態成員函式
    thread t2(A::func2);		//靜態成員函式
    t1.join();
    t2.join();
    a.func3();

}

注意的是, 如果我們選擇將成員函式變成靜態的使用, 那我們就不能使用非靜態的成員變量了, 解決辦法也很簡單, 給靜態成員函式傳遞該物件的 this 指標就好了

以容器存放執行緒物件

我們可以用容器儲存建立的多個執行緒物件, 而當我們像其中插入元素時, 建議使用 emplace_bcak() 而不是 push_back()

我們知道 push_back() 會建立一個臨時物件然後拷貝, 當然自從有了移動語意這裡出發都是移動, 如下例:

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

class A 
{
public:
    void func1() 
    {
        cout << "here is class A`s func 1" << endl;
    }

    void func3() 
    {
        tmpThread.push_back(thread(&A::func1, this));		//(1)
        tmpThread.emplace_back(&A::func1, this);	//(2)
    }
    vector<thread> tmpThread;
};

比較上例中 (1) (2)兩處, 明顯發現emplace_back()push_back() 呼叫形式更加簡潔, 他會自動推導直接根據你給出的引數初始化臨時物件
emplace_back 不會觸發複製構造和移動構造, 他會直接原地構造一個元素
所以使用 emplace_back 更加簡潔效率也更加高

互斥量

std::mutex

mutex 類是保護共享資料, 避免多執行緒同時訪問的同步原語
mutex 也不能複製, 他的operator=被禁用

  • lock
    上鎖, 若失敗則阻塞
  • try_lock
    嘗試上鎖, 失敗則返回
  • unlock
    解鎖

使用時注意死鎖

std::lock_guard

通常不直接使用 mutex, lock_guard 更加安全, 更加方便
他簡化了 lock/unlock 的寫法, lock_guard 在構造時自動鎖定互斥量, 而在退出作用域時會析構自動解鎖, 保證了上鎖解鎖的正確操作, 正是典型的 RAII 機制

#include <thread>
#include <mutex>

std::mutex myLock;
void func() 
{
    {
        std::lock_guard<std::mutex> locker(myLock);   //出作用域自動解鎖
        //do some things...
    }
    myLock.lock();
    myLock.unlock();
}


int main()
{
    std::thread t(func);
    t.join();
}

還有一些其他互斥量, 如std::recursive::mutex 是遞迴型互斥量, 可以讓同一執行緒重複申請等等, 就不一一介紹了

條件變數

條件變數是C++11 提供的一種用於等待的同步機制, 可以阻塞一到多個執行緒, 直到收到另一個執行緒發出的通知或者超時, 才會喚醒當前阻塞的執行緒, 條件變數需要和互斥量配合起來使用

  • std::condition_variable
    該條件變數必須配合 std::unique_lock 使用
  • std::condition_variable_any
    可以和任何帶 lock, unlock 的 mutex 配合使用. 他更加通用, 更加靈活, 但是效率比前者差一些, 使用時會有一些額外的開銷

這兩者具有相同的成員函式

通知

  • notify_one
    喚醒一個阻塞於該條件變數的執行緒

    如果有多個等待的執行緒, 並沒有會優先喚醒誰的說法
    即, 沒有喚醒順序, 是隨機的

  • notify_all
    喚醒所有阻塞於該條件變數的執行緒
    等待
  • wait
    讓當前執行緒阻塞直至條件變數被通知喚醒
  • wait_for
    導致當前執行緒阻塞直至通知條件變數、超過指定時間長度
  • wait_until
    導致當前執行緒阻塞直至通知條件變數、抵達指定時間點

因為虛假喚醒的存在和為了避免丟失訊號量 (在呼叫wait的時候, 在其之前發出的喚醒都不會對wait生效, 而系統不會儲存這些條件變數, 呼叫完就丟掉了), 我們必須使用迴圈判斷條件變數,所以我們使用條件變數必須結合 mutex 並且將判斷條件放入 while 迴圈, 而不是使用 if

call_once

call_once可以保證在多執行緒環境中某一個函式僅僅被呼叫一次, 使用 call_once 需要同時使用其幫助結構體 once_flag

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

once_flag onlyOnce;
mutex myMutex;

void func() //執行緒函式
{
    myMutex.lock();
    cout << "here is func" << endl;
    myMutex.unlock();

    call_once(onlyOnce, []{		//僅僅呼叫一次
        cout << "hello world!" << endl;
    });
}

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

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

    return 0;
}

這篇部落格算是拖了好幾個月才寫的了, 寫一半還沒了, 以後寫部落格記得好好儲存…