UP | HOME

C++ thread 基礎

1. 基本的 thread 建立

標準庫的 thread 非常容易存取

#include <thread>
#include <iostream>

using std::cout;

void hello() {
    cout << "hello" << '\n';
}

int main()
{
    std::thread t(hello);
    t.join();
}
  1. 引入=thread=標頭檔
  2. 宣告函式
  3. 建構一個=thread=物件
  4. 用=join=讓=main=等待它完成

2. 傳入參數

很好,程式會運作,可是我們想要知道如何對 thread 函數傳入參數

void hello(int i) {
    cout << "hello, " << i << '\n';
}

這裡就需要更動函數的宣告式,但呼叫處不能直接寫下直覺的

std::thread t(hello(2));

因為這不是傳入函數,而是傳函數的結果給 thread,那不是我們需要的東西。 而正確的寫法是

std::thread t(hello, 2);

我們可以從這個 Mingw 版本的實作中看到參數怎麼傳進去的

template<class Function, class... Args>
explicit thread(Function&& f, Args&&... args)
{
    typedef decltype(std::bind(f, args...)) Call;
    Call* call = new Call(std::bind(f, args...));
    mHandle = (HANDLE)_beginthreadex(NULL, 0, threadfunc<Call>,
        (LPVOID)call, 0, (unsigned*)&(mThreadId.mId));
    if (mHandle == _STD_THREAD_INVALID_HANDLE)
    {
        int errnum = errno;
        delete call;
        throw std::system_error(errnum, std::generic_category());
    }
}

事實上,我們不只能傳入函數給 Thread,我們可以傳任何可呼叫(callable)物件進去。 用法非常簡單,就是定義一個具有 operator()的 class,然後用這個 class 產生物件。

class Ya {
public:
    void operator()() const {
        cout << "Ya" << '\n';
    }
};

就像這樣

std::thread t( Ya() );

我們用原本的寫法,卻發現編譯失敗,原來是因為這個寫法被編譯器當作函式宣告,而不是一個物件定義。

2.1. 第一種作法:加上括號

std::thread t( (Ya()) );

2.2. 第二種作法:用大括號初始運算子

std::thread t{ Ya() };

第二種作法比較好,用大括號是官方推薦的寫法,而且不會有被誤判的問題。 第一種作法則讓人難以理解為什麼加個括號就能解決問題。

2.3. 第三種作法:用 lambda

再介紹一種作法

std::thread t3([] {
    cout << "lambda" << '\n';
});

利用=lambda=運算式,不過就算是用 lambda 語法也應該用大括號運算子建立 thread。 畢竟,通常沒有理由不用專門用來初始化的大括號。

3. join

那麼=join=呢? =thread=物件一旦建立,啟動執行緒,你就要明確的決定要

  1. 等待執行緒結束(join)
  2. 讓它自己旁邊玩沙(deatch)

如果沒有在 thread 物件被清除之前決定,那程式就會終止。 因為下面的程式中,

~thread()
{
    if (joinable())
        std::terminate();
}

解構子會呼叫 std::terminate() 讓整隻程式掛掉(除非去改變可連結狀態)。

bool joinable() const {return mHandle != _STD_THREAD_INVALID_HANDLE;}

這是=joinable=的實作,因為名稱取的很好,所以可以看出只要 thread 狀態沒有被合法的處理(上面兩個狀況,=join=與=detach=)時會回傳=true=, 在適當的時候引發=terminate=。所以即使發生例外,也要確保執行緒成功被設定好要怎麼處理。 從這裡應該很容易看出來,=thread= 物件可不是 thread 本身,而是 thread 的持有者,千萬不要搞混它們的意義。

4. crash

要讓程式掛掉真的很容易

std::thread t( hello, i );

不決定的結果就是程式=panic=

5. 例外處理

沒有程式遇不到例外,執行緒程式也不例外。 前面我們提到,如果沒有決定如何處理 thread 物件,程式就會異常退出。 問題是,遇到例外時怎麼辦?

5.1. 兩個情況分別 join

第一種辦法很土,不過反正能解決問題就是了

std::thread t(hello);
try {
    // ...
} catch (int err) {
    t.join();
}
t.join();

就是寫兩次,這個方法實際上會帶來很大的問題,因為我們很可能會忘記寫其中一個 join=。 最後除錯追蹤看到 =terminate 然後還想說

我為什麼會呼叫 terminate?

因為你沒有直接呼叫,最後憤怒的挖到 thread 函式庫裡面去。

5.2. 用 RAII 保證

更好的辦法是用 RAII 保證

class Thread_guard {
    std::thread t;
public:
    explicit Thread_guard(std::thread& t_)
        : t{t_}
    {}
    ~Thread_guard() {
        if (t.joinable()) { t.join(); }
    }
}

現在我們只要把 thread 放進去就好了,值得一提的是,這種物件最好移除複製建構子和複製指派運算子。

Thread_guard(Thread_guard const&) = delete;
Thread_guard& operator=(Thread_guard const&) = delete;

兩種操作對這個物件而言都異常危險,我們將無法預測會發生什麼事。 宣告為 delete 之後,試圖做上述操作都會直接被編譯器擋下。 用法非常明確

std::thread t{func}
Thread_guard tg{t}

// do something ...

這樣一來,只要離開資源,物件的解構式被啟動,就會決定怎麼處理 thread 物件。 最後,注意=cout=其實不能那樣用,你可以試試使用迴圈讓執行緒印更多東西,然後你會發現文字會不按順序的亂印。 這是正常的,因為它們交錯的使用=cout=,而沒有一個資源管理的方式。 最簡單的方式就是上鎖,當然也有對這類行為不太介意的程式,例如共享的資源是唯讀的。

Date: 2017-06-26 Mon 00:00
Author: Lîm Tsú-thuàn