c++11線程的使用坑點總結

坑點總結

1、啓動了線程,你需要明確是要等待線程結束(調用join),還是讓其自主運行(調用detach)。如果std::thread對象銷燬之前還沒有做出決定,程序就會終止(std::thread的析構函數會調用std::terminate())。因此,即便是有異常存在,也需要確保線程能夠正確的加入(joined)或分離(detached)。。需要注意的是,必須在std::thread對象銷燬之前做出決定,否則你的程序將會終止(std::thread的析構函數會調用std::terminate(),這時再去決定會觸發相應異常)。
如果不等待線程,就必須保證線程結束之前,可訪問的數據的有效性。這不是一個新問題——單線程代碼中,對象銷燬之後再去訪問,也會產生未定義行爲——不過,線程的生命週期增加了這個問題發生的機率。
這種情況很可能發生在線程還沒結束,函數已經退出的時候,這時線程函數還持有函數局部變量的指針或引用。下面的清單中就展示了這樣的一種情況。

class func1
{
public:  
    func1(int &a) : i(a) {}
    void operator() ()
    {   
        //在這裏調用i變量
    }
private: 
    int &i;
};

void oops()
{
    int local_variable = 0;
    func1 my_func(local_variable);
    std::thread my_thread(my_func);
    my_thread.detach(); //這裏將線程分離,可能local_vriable變量已經銷燬該線程還在運行   
}
  • 這個例子中,已經決定不等待線程結束(使用了detach() ),所以當oops()函數執行完成時,新線程中的函數可能還在運行。如果線程還在運行,它就會訪問已經銷燬的變量。如同一個單線程程序——允許在函數完成後繼續持有局部變量的指針或引用;當然,這從來就不是一個好主意——這種情況發生時,錯誤並不明顯,且會使多線程更容易出錯。
  • 處理這種情況的常規方法:使線程函數的功能齊全,將數據複製到線程中,而非複製到共享數據中。如果使用一個可調用的對象作爲線程函數,這個對象就會複製到線程中,而後原始對象就會立即銷燬。但對於對象中包含的指針和引用還需謹慎,如上程序所示。使用一個能訪問局部變量的函數去創建線程是一個糟糕的主意(除非十分確定線程會在函數完成前結束)。此外,可以通過join()函數來確保線程在函數完成前結束。

2、

class thread_guard
{
public:  
    explicit thread_guard(std::thread &_t) : t(_t) {}
    ~thread_guard()
    {
        if(t.joinable())  //1
        {
            t.join();   //2
        }
    }
    thread_guard(thread_guard const &) = delete; //刪除拷貝構造函數  //3
    thread_guard& operator=(thread_guard const &) = delete; //刪除拷貝賦值函數
private:  
    std::thread &t; //線程的引用
};
void f()
{
    int local_variable = 0;
    func1 my_func(local_variable);
    std::thread t(my_func);
    thread_guard g(t);
    //調用別的函數,如果產生異常,thread_guard的析構函數也會使得線程調用join()加入
    do_something_in_current_thread(); //4 
}

當線程執行到④處時,局部對象就要被逆序銷燬了。因此,thread_guard對象g是第一個被銷燬的,這時線程在析構函數中被加入②到原始線程中。即使do_something_in_current_thread拋出一個異常,這個銷燬依舊會發生。
在thread_guard的析構函數的測試中,首先判斷線程是否可加入①,如果沒有會調用join()②進行加入。這很重要,因爲join()只能對給定的對象調用一次,所以對給已加入的線程再次進行加入操作時,將會導致錯誤。
拷貝構造函數和拷貝賦值操作被標記爲=delete③,是爲了不讓編譯器自動生成它們。直接對一個對象進行拷貝或賦值是危險的,因爲這可能會弄丟已經加入的線程。通過刪除聲明,任何嘗試給thread_guard對象賦值的操作都會引發一個編譯錯誤。想要了解刪除函數的更多知識,請參閱附錄A的A.2節。
如果不想等待線程結束,可以分離_(_detaching)線程,從而避免異常安全(exception-safety)問題。不過,這就打破了線程與std::thread對象的聯繫,即使線程仍然在後臺運行着,分離操作也能確保std::terminate()在std::thread對象銷燬才被調用。

3、std::thread傳遞參數的規則

buffer 是一個指針變量,指向局部變量,然後此局部變量通過 buffer 傳遞到新線程中。此時,函數 oops 很有可能會在 buffer 轉換成std::string對象之前結束,從而導致一些未定義的行爲。因爲此時無法保證隱式轉換的操作和 std::thread 構造函數的拷貝操作按順序進行,有可能 std::thread 的構造函數拷貝的是轉換前的變量(buffer 指針),而非字符串。解決方案就是在傳遞到std::thread構造函數之前就將字面值轉化爲std::string對象:

void f1(int i, std::string const &s);
char buffer[20] = "hello word!";
std::thread t(f1, 3, buffer);
//修改後的
std::thread t(f1, 3, std::string(buffer)); //避免懸垂指針

如果我們想傳遞一個參數的引用,要使用std::ref或者std::cref。

void f(int &num)
{
	++num;
}
int num = 0;
std::thread t(f2, std::ref(num));
t.join();
std::cout << "num = " << num << "\n";

運行結果如圖所示:
在這裏插入圖片描述

4.使用互斥量保護代碼的問題
使用互斥量來保護數據,並不是僅僅在每一個成員函數中都加入一個std::lock_guard對象那麼簡單;一個指針或引用,也會讓這種保護形同虛設。不過,檢查指針或引用很容易,只要沒有成員函數通過返回值或者輸出參數的形式,向其調用者返回指向受保護數據的指針或引用,數據就是安全的。如果你還想深究,就沒這麼簡單了。確保成員函數不會傳出指針或引用的同時,檢查成員函數是否通過指針或引用的方式來調用也是很重要的(尤其是這個操作不在你的控制下時)。函數可能沒在互斥量保護的區域內,存儲着指針或者引用,這樣就很危險。更危險的是:將保護數據作爲一個運行時參數。

class some_data
{
  int a;
  std::string b;
public:
  void do_something();
};

class data_wrapper
{
private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  {
  //std::lock_guard<std::mutex>;作用是將鎖在運行時鎖住,當退出作用域時進行析構,也就會釋放掉互斥鎖
    std::lock_guard<std::mutex> l(m);
    func(data);    // 1 傳遞“保護”數據給用戶函數
  }
};

some_data* unprotected;

void malicious_function(some_data& protected_data)
{
  unprotected=&protected_data;
}

data_wrapper x;
void foo()
{
  x.process_data(malicious_function);    // 2 傳遞一個惡意函數
  unprotected->do_something();    // 3 在無保護的情況下訪問保護數據
}

例子中process_data看起來沒有任何問題,std::lock_guard對數據做了很好的保護,但調用用戶提供的函數func①,就意味着foo能夠繞過保護機制將函數malicious_function傳遞進去②,在沒有鎖定互斥量的情況下調用do_something()。
這段代碼的問題在於根本沒有保護,只是將所有可訪問的數據結構代碼標記爲互斥。函數foo()中調用unprotected->do_something()的代碼未能被標記爲互斥。這種情況下,C++線程庫無法提供任何幫助,只能由開發者使用正確的互斥鎖來保護數據。從樂觀的角度上看,還是有方法可循的:切勿將受保護數據的指針或引用傳遞到互斥鎖作用域之外,無論是函數返回值,還是存儲在外部可見內存,亦或是以參數的形式傳遞到用戶提供的函數中去。
雖然這是在使用互斥量保護共享數據時常犯的錯誤,但絕不僅僅是一個潛在的陷阱而已。下一節中,你將會看到,即便是使用了互斥量對數據進行了保護,條件競爭依舊可能存在。

持續更新…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章