【C++併發實戰】(二)執行緒管理

leno米雷發表於2018-12-11

前一篇沒用markdown編輯器感覺不好看,刪了重新發
本篇主要講述執行緒的管理,主要包括建立和使用執行緒

啟動執行緒

執行緒出現是為了執行任務,執行緒建立時會給一個入口函式,當這個函式返回時,該執行緒就會退出,最常見的main()函式就是主執行緒的入口函式,在main()函式返回時主執行緒就結束了。
如何啟動一個執行緒呢?就如上所述,需要給執行緒物件一個入口函式。

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


void hello()
{
    cout << "hello world";
}

int main()
{
    thread t(hello);
    t.join();
    return 0;
}

上述程式碼中實現了一個簡單的執行緒
一旦執行緒被啟動,就必須要顯式地決定是要等待執行緒完成還是讓執行緒自己執行,這個決定必須要線上程物件銷燬前完成,否則,程式將會被終止(丟擲異常),如果不等待執行緒完成,則需要保證執行緒訪問的資料必須是有效的,直到該執行緒終止,在這種情況下,必須要小心的使用區域性變數,區域性變數的指標和引用。
PS:需要注意的是等待執行緒完成和分離執行緒必須要線上程物件銷燬前完成,並不是線上程函式執行結束前完成。原因是執行緒物件被銷燬後不能直接與執行緒通訊,將無法等待或者分離。
例如:

#include <iostream>
#include <thread>
#include <stdlib.h>
#include <windows.h>
using namespace std;

void hello()
{
    cout << "hello world";
}

int main()
{
    thread t(hello);
    Sleep(3000);
    t.join();
    return 0;
}

這個程式碼中執行緒執行的hello函式已經結束,依然可以join

等待執行緒完成

在上面的程式碼例子中,使用了執行緒物件的join函式,這個函式的作用就是等待執行緒完成。同時需要注意,一個執行緒不能被join兩次。可以通過執行緒物件的joinable()函式判斷當前函式是否可以被join。

std::thread t(do_thread_work);
if(t.joinable())
    t.join();

分離執行緒

除了可以等待執行緒完成,還可以分離執行緒,讓執行緒自己執行,也就是所謂的在後臺執行執行緒,當執行緒物件被銷燬後,執行緒可能仍在執行,此時,沒有直接的方法可以與其通訊,不能再通過執行緒物件獲取到該執行緒,也不能再被join,所以要注意資源的請求和釋放。
參照守護程式的概念,被分離的執行緒通常被稱作守護執行緒。

std::thread t(do_some_background_work);
t.detach();
assert(!t.joinable());

為了分離執行緒,執行緒物件必須與一個執行緒相關聯,不能在沒有執行緒關聯的執行緒物件上使用detach(),join()函式也是同理,因此也可以使用joinable()函式來判斷當前函式是否可以detach。

在異常環境下的等待

如上所述,必須線上程物件被銷燬之前呼叫join()或者detach(),如果需要分離執行緒,一般會線上程物件建立好後立刻呼叫,這樣問題不大。但是如果打算等待執行緒完成,就需要考慮在哪個位置呼叫join。如果線上程開始之後join執行之前發生了異常,對join的呼叫可能就會被跳過。所以應該確保join函式被呼叫,以免程式被終止。

std::thread t(my_function);
try
{
    do_something();
}
catch
{
    t.join();
    throw();
}
t.join();

當然還有其他的做法,利用RAII的思想,類似智慧指標,對thread物件進行封裝。

#include <iostream>
#include <thread>

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


void hello()
{
    std::cout << "hello world";
}

int main()
{
    std::thread t(hello); 
    thread_guard g(t);
    return 0;
}

傳遞引數給執行緒函式

執行緒的入口是個函式,作為使用者肯定是想傳遞一些函式引數的。操作也比較簡單,將額外的引數傳遞給std::thread的夠凹函式就可以了。但是,需要重視的一點是,引數預設上會被複制到內部儲存空間,然後在那新建立的執行緒可以訪問這些引數,即便函式中的相應引數期待著引用
這個意思就是說,傳遞引數給縣城函式就像是給普通函式傳遞引數一樣,預設傳遞的是形參,而且就算你加了引用,傳遞的也是形參。是std::thread的建構函式做的這個事情,他無視函式所期望的引用盲目的複製了所提供的值
解決方案也很簡單,使用std::ref()包裹你想要以引用傳遞的引數。

void update_data_for_widget(widget_id w, widget_data& data);

void fun(widget_id w)
{
    widget_data data;
    std::thread t(update_data_for_widget, w, std::ref(data));
}

如果不這麼做,傳遞的data線上程函式中的改變是不會起作用的。

轉移執行緒的所有權

和std::unique_ptr類似,std::thread是可移動並且非可複製的,這意味著一般不會有兩個執行緒物件指向同一個執行緒例項,如果出現此情況會發生異常。如果同時給一個執行緒物件關聯兩個執行緒例項,也會觸發異常。

void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2 = std::move(t1);
t1 = std::thread(some_other_function);
t3 = std::move(t2);
t1 = std::move(t3);  //此項操作將會終止程式!

標識執行緒

執行緒識別符號是std::thread::id型別的,獲取方式有以下兩種

  • 可以通過與執行緒相關聯的std::thread物件呼叫get_id()方法獲取。如果該執行緒物件沒有相關聯的執行緒,則此方法會返回一個預設構造的std::thread::id物件用來表示“沒有執行緒”
  • 另外可以通過std::this_thread::get_id()獲取當前現成的id。
    id可以自由地被用於複製和比較,或者是被排序。

相關文章