[c++11]多線程編程(一)——初識

什麼是併發

併發在生活中隨處可見,邊走路邊說話,邊聽歌邊寫代碼。計算機術語中的"併發",指的是在單個系統裏同時執行多個獨立的活動,而不是順序的一個接一個的執行。對於單核CPU來講,在某個時刻只可能處理一個任務,但它卻不是徹底執行完一個任務再執行一個下一任務,而是一直在任務間切換,每一個任務完成一點就去執行下一個任務,看起來就像任務在並行發生,雖然不是嚴格的同時執行多個任務,可是咱們仍然稱之爲併發(concurrency)。真正的併發是在在多核CPU上,可以真正的同時執行多個任務,稱爲硬件併發(hardware concurrency)ios

併發並不是沒有代價,在單核CPU併發執行兩個任務須要付出上下文切換的時間代價。以下圖:編程

雙核機器的並行執行與單核機器對比.png

假設A和B兩個任務都被分紅10個大小相等的塊,單核CPU交替的執行兩個任務,每次執行其中一塊,其花費的時間並非先完成A任務再玩成B任務所花費時間的兩倍,而是要更多。這是由於系統從一個任務切換到另外一個任務須要執行一次上下文切換,這是須要時間的(圖中的灰色塊)。上下文切換須要操做系統爲當前運行的任務保存CPU的狀態和指令指針,算出要切換到哪一個任務,併爲要切換的任務從新加載處理器狀態。而後將新任務的指令和數據載入到緩存中。緩存

併發的方式

多進程併發

將應用程序分爲多個獨立的、單線程的進程,他們能夠同時運行。進程內部實現原理比較複雜,這裏就很少說了。安全

併發運行的進程之間的通訊.png

這些獨立的進程能夠經過常規的進程間通訊機制進行通訊,如管道、信號、消息隊列、共享內存、存儲映射I/O、信號量、套接字等。多線程

缺點:併發

  • 進程間通訊較爲複雜,速度相對線程間的通訊更慢。
  • 啓動進程的開銷比線程大,使用的系統資源也更多。

優勢:分佈式

  • 進程間通訊的機制相對於線程更加安全。
  • 可以很容易的將一臺機器上的多進程程序部署在不一樣的機器上(若是通訊機制選取的是套接字的話)。

多線程併發

線程很像輕量級的進程,可是一個進程中的全部線程都共享相同的地址空間,線程間的大部分數據均可以共享。線程間的通訊通常都經過共享內存來實現。函數

併發運行的線程之間的通訊.png

優勢:性能

  • 因爲能夠共享數據,多線程間的通訊開銷比進程小的多。
  • 線程啓動的比進程快,佔用的資源更少。

缺點:測試

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

爲何使用併發

主要緣由有兩個:任務拆分和提升性能。

任務拆分

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

提升性能

在兩種狀況下,併發可以提升性能。

  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());
}

個人簡書連接

參考

  1. C++併發編程實戰
  2. C++ Threading #1: Introduction
相關文章
相關標籤/搜索