C++初級線程管理

前言:

實際程序運行時,每個程序都有一個程序入口,線程也不例外,使用線程時,需要給線程提供一個入口函數,線程執行完入口函數時,線程將退出。C++11中提供瞭std::thread庫,本文將從線程的啟動、線程等待、線程分離、線程傳參、線程識別等幾個方面介紹初級線程管理的知識。

1 線程啟動

C++11中,線程的啟動終究是對std::thread的對象進行構造。

線程構造的類別如下:

1.1  線程函數無參數無返回值

此類可以說是最簡單的線程啟動,函數不需要傳參也不需要返回函數執行結果,執行完成後,線程自動退出。

形如:

void FunDoingNothing();
std::thread(FunDoingNothing)


編寫代碼時,需要加上<thread>頭文件以方便編譯器能夠正確處理thread對象。

1.2  線程函數有參數無返回值

C+=11中,thread的構造函數中使用瞭可變參數,這樣,可以使得構造thread對象時可以自定義傳入參數,

構造函數的定義如下:

template<class F, class... Args> explicit thread(F&& f, Args&&... args);


在實際使用時,線程函數有參數時可以定義形式如下:

void printMsg(int a, int b) {
  cout << "input params are:" << a <<","<<b<< endl;
}
std::thread my_thread(printMsg, 3, 4)


1.3  調用可調用的類型構造

使用時,可以將帶有執行函數的變量傳入thread的構造函數中從而替換默認的構造函數,

如下:

using namespace std;
class BackGroundTask{ 
public:
    void operator()() const{
        doSomeThing();
    }
priavte:
    doSomeThing();
};
int main(){
    BackGroundTask f;
    std::thread myThread(f);
}


上面的代碼中,在啟動線程時同構構造對象f,f對象的重載函數中調用瞭線程運行時要執行的方法。但有一點需要註意的是,在傳入臨時的構造對象時,不經過處理,可能會讓編譯器產生錯誤的理解。

如:

std::thread myThread(BackGroundTask());


這裡相當與聲明瞭一個名為myTread的函數, 這個函數帶有一個參數(函數指針指向沒有參數並返回BackGroundTask對象的函數), 返回一個 std::thread 對象的函數, 而非啟動瞭一個線程。

如果要解決這個問題,隻需要如下處理即可:

std::thread myThread((BackGroundTask()));
std::thread myThread{BackGroundTask()};


當然,也可以使用lamda表達式實現上述功能,如下:

std::thread myThread([]{
  doSomeThing();
  });


2 等待線程

C++11中,確保線程執行完後,主線程在退出,需要在代碼中使用join()函數,這樣就可以保證變量在線程結束時才會進行銷毀。

2.1 join等待

在實際編程時,join函數隻是簡單的等待或者不等待。在有些場景下就會不使用,如果想要進行更加靈活的控制,需要使用C++11中提供的其他機制,這個也會在後面的推文中進行說明。
在編程時,如果對一個線程使用瞭join,那麼在後續的操作中如果使用joinable()執行結果將返回false。既一旦使用瞭join。線程對象將不能重復使用。如下代碼中,在線程中使用join等待。

class BackGroundTask
{ 
public:
    void operator()()
{
        doSomeThing();
    } 
private:
    void doSomeThing() {cout<<"線程退出"<<endl;};
};
int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    myThread.join();
    cout<<"退出"<<endl;
}


上面的代碼使用瞭線程等待,可以輸出正確的結果,如下:

線程退出
退出

如果將 myThread.join()語句註釋,再次執行時,程序將執行出錯,因為在子線程還沒有結束時,主線程已經結束。

運行結果如下:

退出
terminate called without an active exception

上面的輸出具備不確定性,代碼運行時結果隨機。

2.2 異常場景的join等待

異常場景中,如果沒有充分考慮join的位置,就可能會產生因為異常導致主線程先於子線程退出的情況,解決這些問題可以通過下面兩種方法進行處理:

2.2.1  通過異常捕獲

通過分析代碼中的異常場景,對異常使用try...catch進行捕獲,然後在需要線程等待的地方調用join()函數,這種方法雖然可以輕易地捕獲問題並對問題進行修復,但並非是通用法則,還需要根據實際情況進行分析。如檢查並確認是否線程函數中是否使用瞭局部變量的引用等其它原因。

2.2.2 使用RAII方式進行線程等待

RAII可以理解為資源獲取既初始化。因為全寫為:Resource Acquisition Is Initialization
實際使用時,通過定義一個類,然後在析構函數中使用join函數進行線程等待。這樣可以避免場景有遺漏的地方。

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


 如上,通過在將線程對象傳入到類thread_guard中,如果thread_guard類對象的局部變量被銷毀,則在析構函數中會將線程托管到原始線程。
thread_guard中,使用delete標識,禁止生成該類的默認拷貝構造、以及賦值函數。
在實際編程時如果不想線程等待,可以使用detach方法,將線程和主線程進行分離。

3 線程分離

線程分離使用detach方法,使用後將不能在對已分離的線程進行管理,但是分離的線程可以真實的在後臺進行運行。當線程退出時,C++會對線程資源進行清理和回收。
線程分離通常被用作守護線程或者後臺工作線程。

使用方法如下:

int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    myThread.detach();
    cout<<"退出"<<endl;
}


4 向線程傳遞參數

向線程傳遞參數非常簡單,在上面的代碼中也有提及,這裡主要說下向線程中傳遞參數的陷阱。

看下面的代碼:

void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; 
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}


上面的代碼中buffer是一個局部指針變量,使用後,可能會導致線程出現未定義的行為,因為從char*到string的轉換時使用的是隱式轉換,但是thread在使用時會將變量拷貝到線程私有內存,但是並不知道需要將參數進行轉換,因此復制到私有內存的變量就沒有轉換成期望的對象。
如果要解決這個問題,可以在使用時直接將參數類型轉換成函數默認的類型,在上面的例子中可以

做如下操作:

std::thread t(f,3,std::string(buffer));


但是這樣做依然存在問題,既線程在復制變量到私有內存時,隻復制瞭變量值,這樣在線程調用後,如果繼續使用線程函數處理後的變量時可能變量並沒有改造,依舊是線程調用之前的變量。
因此要想在函數傳參過程中使得線程拷貝時依舊保持引用,可以在線程調用時使用引用方式,

如:

std::thread t(f,3,std::ref(std::string(buffer)));


5 線程識別

每個線程都有一個線程標識,在C++11中,線程標識通過std::thread::id進行標識,std::thread::id可以復用並進行比較,如果兩個線程的id相等,那麼它們就是同一個線程或者沒有線程,如果不等就表示兩個是不同的線程或者其中一個線程不存在。

線程id的獲取方法有兩種,如下:

5.1 thread成員函數獲取

通過std::thread::get_id()可以獲取線程的id。

使用方法如下:

int main()
{
    BackGroundTask f;
    std::thread myThread(f);
    cout<<"線程id:"<<myThread.get_id()<<endl;
    myThread.detach();
    cout<<"退出"<<endl;
}


線程運行結果為:

  • 線程id:139879559096064
  • 退出

5.2 std::this_thread::get_id()

線程id可以用來區分主線程和子線程,通過std::this_thread::get_id()可以先將主線程id保存,然後在和子線程進行比較,從而區分主線程和子線程。

代碼如下:

int main()
{
    std::thread::id master_thread=std::this_thread::get_id();
    BackGroundTask f;
    std::thread myThread(f);
    if(master_thread!=myThread.get_id())
    {
        cout<<"子線程id:"<<myThread.get_id()<<endl;
    }
    myThread.detach();
    cout<<"退出"<<endl;
}


代碼中,先保存瞭主線程的id標識,然後獲取子線程id,比較兩個線程id。如果不相等則輸出子線程id。

代碼運行結果如下:

子線程id:140161423791872

到此這篇關於C++初級線程管理的文章就介紹到這瞭,更多相關C++線程管理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: