第25課 std::thread對象的析構

一. 線程的等待與分離ios

(一)join和detach函數程序員

  1. 線程等待:join()編程

  (1)等待子線程結束,調用線程處於阻塞模式緩存

  (2)join()執行完成以後,底層線程id被設置爲0,即joinable()變爲false。同時會清理線程相關的存儲部分, 這樣 std::thread 對象將再也不與已經底層線程有任何關聯。這意味着,只能對一個線程使用一次join();調用join()後,joinable()返回false。安全

  2. 線程分離:detach()數據結構

  (1)分離子線程,與當前線程的鏈接被斷開,子線程成爲後臺線程,被C++運行時庫接管。這意味着不可能再有std::thread對象能引用到子線程了。與join同樣,detach也只能調用一次,當detach之後其joinable()爲false函數

  (2)注意事項:oop

    ①若是不等待線程,就必須保證線程結束以前,可訪問的數據是有效的。特別是要注意線程函數是否還持有一些局部變量的指針或引用性能

    ②爲防止上述的懸空指針和懸引用的問題,線程對象的生命期應儘可能長於底層線程的生命期測試

  (3)應用場合

    ①適合長時間運行的任務,如後臺監視文件系統、對緩存進行清理、對數據結構進行優化等。

    ②線程被用於「發送即無論」(fire and forget)的任務,任務完成狀況線程並不關心,即安排好任務以後就無論。

(二)聯結狀態:一個std::thread對象只可能處於可聯結或不可聯結兩種狀態之一可用joinable()函數來判斷,即std::thread對象是否與某個有效的底層線程關聯(內部經過判斷線程id是否爲0來實現)。

  1. 可聯結(joinable):當線程可運行、己運行或處於阻塞時是可聯結的。注意,若是某個底層線程已經執行完任務可是沒有被join的話,該線程依然會被認爲是一個活動的執行線程,仍然處於joinable狀態

  2. 不可聯結(unjoinable):

  (1)當不帶參構造的std::thread對象爲不可聯結,由於底層線程還沒建立。

  (2)己移動的std::thread對象爲不可聯結。由於該對象的底層線程id會被設置爲0。

  (3)己調用join或detach的對象爲不可聯結狀態。由於調用join()之後,底層線程己結束,而detach()會把std::thread對象和對應的底層線程之間的鏈接斷開。

【編程對象】等待與分離

#include <iostream>
#include <thread>

using namespace std;

//1. 懸空引用問題
class FuncObject
{
    void do_something(int& i) { cout <<"do something: " << i << endl; }
public:
    int& i;
    FuncObject(int& i) :i(i) {  }

    void operator()()
    {
        for (unsigned int j = 0; j < 1000; ++j)
        {
            do_something(i); //可能出現懸空引用的問題。
        }
    }
};

void oops()
{
    int localVar = 0;
    FuncObject fObj(localVar);

    std::thread t1(fObj);

    t1.detach();  //子線程分離,轉爲後臺運行。主線程調用oops函數,可能出現oops函數
                  //執行完了,子線程還在運行的現象。它會去調用do_something,這時會
                  //訪問到己經被釋放的localVar變量,會出現未定義行爲!若是這裏改爲
                  //join()則不會發生這種現象。所以主線程會等子線程執行完才退出oops
}


//2. 利用分離線程處理多文檔文件
void openDocAndDisplay(const std::string& fileName){} //打開文件
bool doneEditing() { return false; } //判斷是否結束編輯
enum class UserCommand{OpenNewDocument, SaveDocument,EditDocument}; //命令類型
UserCommand getUserInput() { return UserCommand::EditDocument; } //獲取用戶命令
string getFilenameFromUser() { return ""; } //獲取文件名
void processUserInput(UserCommand cmd){} //處理其它命令

void editDocument(const std::string& fileName) 
{
    openDocAndDisplay(fileName);

    while (!doneEditing()) {
        UserCommand cmd = getUserInput();
        if (cmd == UserCommand::OpenNewDocument) { //若是用戶選擇打開一個新文檔
            const string  newName = getFilenameFromUser();
            std::thread t(editDocument, newName);  //啓動新線程去處理這個新文檔

            t.detach(); //子線程分離。這樣主線程就能夠繼續處理其餘任務。
        }else {
            processUserInput(cmd);
        }
    }
}

int main()
{
    //1. 懸空引用問題
    oops();

    //2. 利用分離線程處理多文檔文件
    editDocument("E:\\Demo\\abc.doc");
    return 0;
}

二. std::thread對象的析構

(一)std::thread的析構

  1. std::thread對象析構時,會先判斷joinable(),若是可聯結,則程序會直接被終止(terminate)

  2. 這意味std::thread對象從其它定義域出去的任何路徑,都應爲不可聯結狀態。也意味着建立thread對象之後,要在隨後的某個地方顯式地調用join或detach以便讓std::thread處於不可聯結狀態。

(二)爲何析構函數中不隱式調用join或detach

  1. 若是設計成隱式join():將致使調用線程一直等到子線程結束才返回。若是子線程正在運行一個耗時任務,這可能形成性能低下的問題,並且問題也不容易被發現

  2. 若是設計成隱式detach():因爲detach會將切斷std::thread對象與底層線程之間的關聯,兩個線程今後各自獨立運行。若是線程函數是按引用(或指針)方式捕捉的變量,在調用線程退出做用域後這些變量會變爲無效,這容易掩蓋錯誤也將使調試更加困難。所以隱式detach,還不如join或者顯式調用detach更直觀和安全。

  3.標準委員會認爲,銷燬一個joinable線程的後果是十分可怕的,所以他們經過terminate程序來禁止這種行爲。爲了不銷燬一個joinable的線程,就得由程序員本身來確保std::thread對象從其定義的做用域出去的任何路徑,都處於不可聯結狀態,最經常使用的方法就是資源獲取即初始化技術RAII,Resource Acquisition Is Initialization)。

(三)std::thread對象與RAII技術的結合

  1. 方案1:自定義的thread_guard類,並將std::thread對象傳入其中,同時在構造時選擇join或detach策略。當thread_guard對象析構時,會根據析構策略,調用std::thread的join()或detach(),確保在任何路徑,線程對象都處於unjoinable狀態。

  2. 方案2:從新封裝std::thread類(見下面的代碼,類名爲joining_thread),在析構時隱式調用join()。

【編程實驗】利用RAII確保std::thread全部路徑皆爲unjoinable

#include <iostream>
#include <thread>
#include <functional>
#include <algorithm>

using namespace std;

constexpr auto tenMillion = 10000000;

bool conditionsAreSatisfied() { return false;}//return true or false

//問題函數:doWork_oops(沒有確保std::thread全部皆爲不可聯結)
//參數:filter過濾器,選0至maxVal之間的值選擇出來並放入vector中
bool doWork_oops(std::function<bool(int)> filter, int maxVal = tenMillion)
{
    std::vector<int> goodVals; //保存通過濾器篩選出來的數值(0-maxVal)

    std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部變量,按引用傳入子線程。
                    for (auto i = 0; i <= maxVal; ++i)
                        if (filter(i))  goodVals.push_back(i);
                });

    if (conditionsAreSatisfied()) { //若是一切就緒,就開始計算任務
        t.join(); //等待子線程結束
        //performComputation(goodVals);  //主線程執行計算任務
        return true;
    }

    //conditionsAreSatisfied()時false,表示條件不知足。(注意,仍沒調用join()或detach())
    return  false; //調用線程(通常是主線程)執行到這裏,t對象被析構,std::thread的析構函數被調用,
                   //此時因爲子線程仍處於可聯結狀態,將執行std::ternimate終止程序!
                   //爲何std::thread析構函數不隱式執行join或detach,而是終止程序的運行?
                   //若是隱式調用join()會讓主線程等待子線程(耗時任務)結束,這會浪費性能。
                   //而若是隱式調用detach會使主線程和子線程分離,子線程因爲引用goodVals局部變量,
                   //會出現懸空引用的問題,但這問題又不容易被發現。所以,經過std::ternimate來終止
                   //程序,以便讓程序員本身決定和消除這些問題。好比繼續調用join(),仍是detach(但需
                   //要同時解決懸空引用問題)?
}

//利用RAII技術,確保std::thread的正常析構
class thread_guard  //scoped_thread
{
public:
    enum class DtorAction{join, detach}; //析構行爲

    //構造函數只接受右值類型,由於std::thread只能被移動。雖然t爲右值引用類型,但因爲形參自己
    //左值,所以調用std::move將形參轉爲右值。
    thread_guard(std::thread&& t, DtorAction a = DtorAction::join):action(a), thr(std::move(t))
    {
    }

    ~thread_guard()
    {
        if (thr.joinable()) //必須校驗,join和detach只能被調用一次
        {
            if (action == DtorAction::join) {
                thr.join();
            } else {
                thr.detach();
            }
        }
    }

    std::thread& get() { return thr; }

    //因爲聲明瞭析構函數,編譯器將再也不提供移動操做函數,所以需手動生成
    thread_guard(thread_guard&&) noexcept = default;
    thread_guard& operator=(thread_guard&&) = default;

    //本類不支持複製
    thread_guard(const thread_guard&) = delete;
    thread_guard& operator=(const thread_guard&) = delete;
private:

    //注意action和thr的聲明順序,因爲thr被建立之後會執行起來,必須
    //保證action己被初始化。所以先聲明action,再聲明thr。
    DtorAction action;
    std::thread thr;
};

bool doWork_ok(std::function<bool(int)> filter, int maxVal = tenMillion)
{
    std::vector<int> goodVals;

    std::thread t([&filter, maxVal, &goodVals] { //注意goodVals是局部變量,按引用傳入子線程。
        for (auto i = 0; i <= maxVal; ++i)
            if (filter(i)) 
            { 
                cout << i << endl;
                goodVals.push_back(i); 
            }
        });

    thread_guard guard(std::move(t));//默認析構策略是thread_guard::DtorAction::join

    if (conditionsAreSatisfied()) { //若是一切就緒,就開始計算任務
        guard.get().join(); //等待子線程結束
        //performComputation(goodVals);  //主線程執行計算任務
        return true;
    }

    //conditionsAreSatisfied()時false,表示條件不知足。guard對象析構,但會隱式調std::thread對象
    //的join()。
    return  false;
}

//使用RAII等待線程完成:joining_thread類的實現
class joining_thread
{
    std::thread thr;
public:
    joining_thread() noexcept = default;

    //析構函數
    ~joining_thread()
    {
        if (joinable()) //對象析構造,會隱式調用join()
        {
            join();
        }
    }

    template<typename Callable, typename... Args>
    explicit joining_thread(Callable&& func, Args&& ...args):
        thr(std::forward<Callable>(func), std::forward<Args>(args)...)
    {
    }

    //類型轉換構造函數
    explicit joining_thread(std::thread t) noexcept : thr(std::move(t))
    {
    }

    //移動操做
    joining_thread(joining_thread&& other) noexcept : thr(std::move(other.thr))
    {
    }

    joining_thread& operator=(joining_thread&& other) noexcept
    {
        if (joinable()) join(); //等待原線程執行完

        thr = std::move(other.thr); //將新線程移動到thr中

        return *this;
    }

    joining_thread& operator=(std::thread other) noexcept
    {
        if (joinable()) join();

        thr = std::move(other);

        return *this;
    }
    
    bool joinable() const noexcept
    {
        return thr.joinable();
    }

    void join() { thr.join(); }
    void detach() { thr.detach(); }

    void swap(joining_thread& other) noexcept { thr.swap(other.thr); }
    std::thread::id get_id() const noexcept { return thr.get_id(); }

    std::thread& asThread() noexcept //轉化爲std::thread對象
    {
        return thr;
    }
    const std::thread& asThread() const noexcept
    {
        return thr;
    }
};

void doWork(int i) { cout << i << endl; }

int main()
{
    //1.問題函數:doWork_oops:沒有確保std::thread的全部路徑都爲joinable
    //doWork_oops([](auto val) { return val >= 100; }, 1000);

    //2. doWork_ok函數
    doWork_ok([](auto val) { return val >= 100; }, 1000);

    //3. 測試joining_thread類
    std::vector<joining_thread> threads;  //joining_thread析構時隱式調用join
    for (unsigned int i = 0; i < 20; ++i) {
        threads.push_back(joining_thread(doWork, i));
    }

    std::for_each(threads.begin(), threads.end(), std::mem_fn(&joining_thread::join));

    return 0;
}
相關文章
相關標籤/搜索