2017年6月26日 星期一

C++ 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等待它完成

很好,程式應該會運作,可是我們想要知道如何傳入參數,對吧!
void hello(int i) {
    cout << "hello, " << i << '\n';
}
所以函數的宣告式自然要改
但是我們不能直接寫
std::thread t(hello(2));
因為這不會傳入函數,而是傳函數的結果,那不是我們需要的東西
正確的寫法是
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() );
我們用原本的寫法,卻發現編譯失敗,原來是因為這個寫法被編譯器當作函式宣告,而不是一個物件定義

好吧!怎麼處理?
第一種作法:加上括號
std::thread t( (Ya()) );
第二種作法:用大括號初始運算子
std::thread t{ Ya() };
第二種作法自然必較好,因為符合新的標準(用大括號是官方推薦寫法),而且很直觀
第一種作法則讓人難以理解為什麼這樣就可以

再介紹一種作法
std::thread t3([] {
    cout << "lambda" << '\n';
});
利用lambda運算式,不過就算是用lambda我也認為應該用大括號運算子,畢竟,沒什麼道理不用擺明用來初始化的大括號(我是說,除了那個該死的auto array狀況,還有字串字面值是const char *)

那麼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本身,而是持有者,所以千萬不要搞混它們的意義

要讓程式掛掉真的很容易
std::thread t( hello, i );
不決定的結果就是程式panic

例外!!!
沒錯,什麼程式遇不到例外,執行緒程式也不例外,前面我們提到,如果沒有決定如何處理thread物件,程式就會掛掉
很好,那麼遇到例外時怎麼辦?
第一種辦法很土,不過反正能解決問題就是了
std::thread t(hello);
try {
    // ...
} catch (int err) {
    t.join();
}
t.join();
看,就是寫兩次而已,這真的很糟糕
因為我們很可能會忘記寫某一個join,然後沒看到,或是當下看不出來,最後trace bug還看到terminate然後想----我為什麼會呼叫terminate?恩,因為你沒有呼叫,最後憤怒的找到thread函式庫

第二種辦法是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 ...
這樣一來,只要離開資源,tg的解構式被啟動,就會決定怎麼處理thread物件(這仰賴c++對解構的保證)

最後,注意cout其實不能那樣用,你可以試試使用迴圈讓執行緒印更多東西,然後你會發現文字會不按順序的亂印,這是正常的,因為它們交錯的使用cout,而沒有一個資源管理的方式
最簡單的方式就是上鎖,當然也有對這類行為不太介意的程式,例如共享的資源是唯獨的

沒有留言:

張貼留言