C++11---多線程編程(一)

線程很像輕量級的進程,但是一個進程中的所有線程都共享相同的地址空間,線程間的大部分數據都可以共享。線程間的通信一般都通過共享內存來實現。

 

優點:

  • 由於可以共享數據,多線程間的通信開銷比進程小的多。
  • 線程啓動的比進程快,佔用的資源更少。

缺點:

  • 共享數據太過於靈活,爲了維護正確的共享,代碼寫起來比較複雜。
  • 無法部署在分佈式系統上。

爲什麼使用併發

主要原因有兩個:任務拆分和提高性能。

任務拆分

在編寫軟件的時候,將相關的代碼放在一起,將無關的代碼分開,這是一個好主意,這樣能夠讓程序更加容易理解和測試。將程序劃分成不同的任務,每個線程執行一個任務或者多個任務,可以將整個程序的邏輯變得更加簡單。

提高性能

在兩種情況下,併發能夠提高性能。

  1. 任務並行(task parallelism):將一個單個任務分成若干個部分各自並行運行,從而降低運行時間。雖然聽起來很簡單,但其實是一個相當複雜的過程,設想假如各個部分之間存在很多以來,一個部分的執行需要使用到另一個任務的執行結果,這個時候並不能很好的並行完成。
  2. 數據並行(data parallelism):每個線程在不同的數據部分上執行相同的操作。

多線程庫簡介

C++98標準中並沒有線程庫的存在,而在C++11中終於提供了多線程的標準庫,提供了管理線程、保護共享數據、線程間同步操作、原子操作等類。

多線程庫對應的頭文件是#include <thread>,類名爲std::thread

#include <iostream>
#include <thread>

void function_1() {
    std::cout << "I'm function_1()" << std::endl;
}

int main() {
    function_1();
    return 0;
}

這是一個典型的單線程的單進程程序,任何程序都是一個進程,main()函數就是其中的主線程,單個線程都是順序執行。

將上面的程序改造成多線程程序其實很簡單,讓function_1()函數在另外的線程中執行:

#include <iostream>
#include <thread>

void function_1() {
    std::cout << "I'm function_1()" << std::endl;
}

int main() {
    std::thread t1(function_1);
    // do other things
    t1.join();
    return 0;
}

分析

  1. 首先,構建一個std::thread對象t1,構造的時候傳遞了一個參數,這個參數是一個函數,這個函數就是這個線程的入口函數,函數執行完了,整個線程也就執行完了。
  2. 線程創建成功後,就會立即啓動,並沒有一個類似start的函數來顯式的啓動線程。
  3. 一旦線程開始運行, 就需要顯式的決定是要等待它完成(join),或者分離它讓它自行運行(detach)。注意:只需要在std::thread對象被銷燬之前做出這個決定。這個例子中,對象t1是棧上變量,在main函數執行結束後就會被銷燬,所以需要在main函數結束之前做決定。
  4. 這個例子中選擇了使用t1.join(),主線程會一直阻塞着,直到子線程完成,join()函數的另一個任務是回收該線程中使用的資源。

線程對象和對象內部管理的線程的生命週期並不一樣,如果線程執行的快,可能內部的線程已經結束了,但是線程對象還活着,也有可能線程對象已經被析構了,內部的線程還在運行。

假設t1線程是一個執行的很慢的線程,主線程並不想等待子線程結束就想結束整個任務,直接刪掉t1.join()是不行的,程序會被終止(析構t1的時候會調用std::terminate,程序會打印terminate called without an active exception)。

與之對應,我們可以調用t1.detach(),從而將t1線程放在後臺運行,所有權和控制權被轉交給C++運行時庫,以確保與線程相關聯的資源在線程退出後能被正確的回收。參考UNIX守護進程(daemon process)的概念,這種被分離的線程被稱爲守護線程(daemon threads)。線程被分離之後,即使該線程對象被析構了,線程還是能夠在後臺運行,只是由於對象被析構了,主線程不能夠通過對象名與這個線程進行通信。例如:

#include <iostream>
#include <thread>

void function_1() {
    //延時500ms 爲了保證test()運行結束之後纔打印
    std::this_thread::sleep_for(std::chrono::milliseconds(500)); 
    std::cout << "I'm function_1()" << std::endl;
}

void test() {
    std::thread t1(function_1);
    t1.detach();
    // t1.join();
    std::cout << "test() finished" << std::endl;
}

int main() {
    test();
    //讓主線程晚於子線程結束
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //延時1s
    return 0;
}

// 使用 t1.detach()時
// test() finished
// I'm function_1()

// 使用 t1.join()時
// I'm function_1()
// test() finished

分析:

  1. 由於線程入口函數內部有個500ms的延時,所以在還沒有打印的時候,test()已經執行完成了,t1已經被析構了,但是它負責的那個線程還是能夠運行,這就是detach()的作用。
  2. 如果去掉main函數中的1s延時,會發現什麼都沒有打印,因爲主線程執行的太快,整個程序已經結束了,那個後臺線程被C++運行時庫回收了。
  3. 如果將t1.detach()換成t1.join()test函數會在t1線程執行結束之後,纔會執行結束。

一旦一個線程被分離了,就不能夠再被join了。如果非要調用,程序就會崩潰,可以使用joinable()函數判斷一個線程對象能否調用join()

void test() {
    std::thread t1(function_1);
    t1.detach();

    if(t1.joinable())
        t1.join();

    assert(!t1.joinable());
}

 

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