[轉]C++ 11 多線程--線程管理

轉載地址:https://www.cnblogs.com/wangguchangqing/p/6134635.htmlcss

說到多線程編程,那麼就不得不提並行併發,多線程是實現併發(並行)的一種手段。並行是指兩個或多個獨立的操做同時進行。注意這裏是同時進行,區別於併發,在一個時間段內執行多個操做。在單核時代,多個線程是併發的,在一個時間段內輪流執行;在多核時代,多個線程能夠實現真正的並行,在多核上真正獨立的並行執行。例如如今常見的4核4線程能夠並行4個線程;4核8線程則使用了超線程技術,把一個物理核模擬爲2個邏輯核心,能夠並行8個線程。html

併發編程的方法

一般,要實現併發有兩種方法:多進程和多線程。node

多進程併發

使用多進程併發是將一個應用程序劃分爲多個獨立的進程(每一個進程只有一個線程),這些獨立的進程間能夠互相通訊,共同完成任務。因爲操做系統對進程提供了大量的保護機制,以免一個進程修改了另外一個進程的數據,使用多進程比多線程更容易寫出安全的代碼。但這也造就了多進程併發的兩個缺點:ios

  • 在進程件的通訊,不管是使用信號、套接字,仍是文件、管道等方式,其使用要麼比較複雜,要麼就是速度較慢或者二者兼而有之。
  • 運行多個線程的開銷很大,操做系統要分配不少的資源來對這些進程進行管理。

因爲多個進程併發完成同一個任務時,不可避免的是:操做同一個數據和進程間的相互通訊,上述的兩個缺點也就決定了多進程的併發不是一個好的選擇。程序員

多線程併發

多線程併發指的是在同一個進程中執行多個線程。有操做系統相關知識的應該知道,線程是輕量級的進程,每一個線程能夠獨立的運行不一樣的指令序列,可是線程不獨立的擁有資源,依賴於建立它的進程而存在。也就是說,同一進程中的多個線程共享相同的地址空間,能夠訪問進程中的大部分數據,指針和引用能夠在線程間進行傳遞。這樣,同一進程內的多個線程可以很方便的進行數據共享以及通訊,也就比進程更適用於併發操做。因爲缺乏操做系統提供的保護機制,在多線程共享數據及通訊時,就須要程序員作更多的工做以保證對共享數據段的操做是以預想的操做順序進行的,而且要極力的避免死鎖(deadlock)編程

C++ 11的多線程初體驗

C++11的標準庫中提供了多線程庫,使用時須要#include <thread>頭文件,該頭文件主要包含了對線程的管理類std::thread以及其餘管理線程相關的類。下面是使用C++多線程庫的一個簡單示例:安全

#include <iostream> #include <thread> using namespace std; void output(int i) { cout << i << endl; } int main() { for (uint8_t i = 0; i < 4; i++) { thread t(output, i); t.detach(); } getchar(); return 0; }

在一個for循環內,建立4個線程分別輸出數字0、一、二、3,而且在每一個數字的末尾輸出換行符。語句thread t(output, i)建立一個線程t,該線程運行output,第二個參數i是傳遞給output的參數。t在建立完成後自動啓動,t.detach表示該線程在後臺容許,無需等待該線程完成,繼續執行後面的語句。這段代碼的功能是很簡單的,若是是順序執行的話,其結果很容易預測獲得markdown

0 \n 1 \n 2 \n 3 \n 

可是在並行多線程下,其執行的結果就多種多樣了,下圖是代碼一次運行的結果:
多線程

能夠看出,首先輸出了01,並無輸出換行符;緊接着卻連續輸出了2個換行符。不是說好的並行麼,同時執行,怎麼還有前後的順序?這就涉及到多線程編程最核心的問題了資源競爭。CPU有4核,能夠同時執行4個線程這是沒有問題了,可是控制檯卻只有一個,同時只能有一個線程擁有這個惟一的控制檯,將數字輸出。將上面代碼建立的四個線程進行編號:t0,t1,t2,t3,分別輸出的數字:0,1,2,3。參照上圖的執行結果,控制檯的擁有權的轉移以下:併發

  • t0擁有控制檯,輸出了數字0,可是其沒有來的及輸出換行符,控制的擁有權卻轉移到了t1;(0)
  • t1完成本身的輸出,t1線程完成 (1\n)
  • 控制檯擁有權轉移給t0,輸出換行符 (\n)
  • t2擁有控制檯,完成輸出 (2\n)
  • t3擁有控制檯,完成輸出 (3\n)

因爲控制檯是系統資源,這裏控制檯擁有權的管理是操做系統完成的。可是,假如是多個線程共享進程空間的數據,這就須要本身寫代碼控制,每一個線程什麼時候可以擁有共享數據進行操做。共享數據的管理以及線程間的通訊,是多線程編程的兩大核心。

線程管理

每一個應用程序至少有一個進程,而每一個進程至少有一個主線程,除了主線程外,在一個進程中還能夠建立多個線程。每一個線程都須要一個入口函數,入口函數返回退出,該線程也會退出,主線程就是以main函數做爲入口函數的線程。在C++ 11的線程庫中,將線程的管理在了類std::thread中,使用std::thread能夠建立、啓動一個線程,並能夠將線程掛起、結束等操做。

啓動一個線程

C++ 11的線程庫啓動一個線程是很是簡單的,只須要建立一個std::thread對象,就會啓動一個線程,並使用該std::thread對象來管理該線程。

do_task();
std::thread(do_task);

這裏建立std::thread傳入的函數,實際上其構造函數須要的是可調用(callable)類型,只要是有函數調用類型的實例都是能夠的。全部除了傳遞函數外,還可使用:

  • lambda表達式

使用lambda表達式啓動線程輸出數字

for (int i = 0; i < 4; i++) { thread t([i]{ cout << i << endl; }); t.detach(); }
  • 重載了()運算符的類的實例

使用重載了()運算符的類實現多線程數字輸出

class Task { public: void operator()(int i) { cout << i << endl; } }; int main() { for (uint8_t i = 0; i < 4; i++) { Task task; thread t(task, i); t.detach(); } }

把函數對象傳入std::thread的構造函數時,要注意一個C++的語法解析錯誤(C++'s most vexing parse)。向std::thread的構造函數中傳入的是一個臨時變量,而不是命名變量就會出現語法解析錯誤。以下代碼:

std::thread t(Task());

這裏至關於聲明瞭一個函數t,其返回類型爲thread,而不是啓動了一個新的線程。可使用新的初始化語法避免這種狀況

std::thread t{Task()};

當線程啓動後,必定要在和線程相關聯的thread銷燬前,肯定以何種方式等待線程執行結束。C++11有兩種方式來等待線程結束

  • detach方式,啓動的線程自主在後臺運行,當前的代碼繼續往下執行,不等待新線程結束。前面代碼所使用的就是這種方式。
  • join方式,等待啓動的線程完成,纔會繼續往下執行。假如前面的代碼使用這種方式,其輸出就會0,1,2,3,由於每次都是前一個線程輸出完成了纔會進行下一個循環,啓動下一個新線程。

不管在何種情形,必定要在thread銷燬前,調用t.join或者t.detach,來決定線程以何種方式運行。當使用join方式時,會阻塞當前代碼,等待線程完成退出後,纔會繼續向下執行;而使用detach方式則不會對當前代碼形成影響,當前代碼繼續向下執行,建立的新線程同時併發執行,這時候須要特別注意:建立的新線程對當前做用域的變量的使用,建立新線程的做用域結束後,有可能線程仍然在執行,這時局部變量隨着做用域的完成都已銷燬,若是線程繼續使用局部變量的引用或者指針,會出現意想不到的錯誤,而且這種錯誤很難排查。例如:

auto fn = [](int *a){ for (int i = 0; i < 10; i++) cout << *a << endl; }; []{ int a = 100; thread t(fn, &a); t.detach(); }();

在lambda表達式中,使用fn啓動了一個新的線程,在裝個新的線程中使用了局部變量a的指針,而且將該線程的運行方式設置爲detach。這樣,在lamb表達式執行結束後,變量a被銷燬,可是在後臺運行的線程仍然在使用已銷燬變量a的指針,其輸出結果以下:

只有第一個輸出是正確的值,後面輸出的值是a已被銷燬後輸出的結果。因此在以detach的方式執行線程時,要將線程訪問的局部數據複製到線程的空間(使用值傳遞),必定要確保線程沒有使用局部變量的引用或者指針,除非你能確定該線程會在局部做用域結束前執行結束。固然,使用join方式的話就不會出現這種問題,它會在做用域結束前完成退出。

異常狀況下等待線程完成

當決定以detach方式讓線程在後臺運行時,能夠在建立thread的實例後當即調用detach,這樣線程就會後thread的實例分離,即便出現了異常thread的實例被銷燬,仍然能保證線程在後臺運行。但線程以join方式運行時,須要在主線程的合適位置調用join方法,若是調用join前出現了異常,thread被銷燬,線程就會被異常所終結。爲了不異常將線程終結,或者因爲某些緣由,例如線程訪問了局部變量,就要保證線程必定要在函數退出前完成,就要保證要在函數退出前調用join

void func() { thread t([]{ cout << "hello C++ 11" << endl; }); try { do_something_else(); } catch (...) { t.join(); throw; } t.join(); }

上面代碼可以保證在正常或者異常的狀況下,都會調用join方法,這樣線程必定會在函數func退出前完成。可是使用這種方法,不但代碼冗長,並且會出現一些做用域的問題,並非一個很好的解決方法。

一種比較好的方法是資源獲取即初始化(RAII,Resource Acquisition Is Initialization),該方法提供一個類,在析構函數中調用join

class thread_guard { thread &t; public : explicit thread_guard(thread& _t) : t(_t){} ~thread_guard() { if (t.joinable()) t.join(); } thread_guard(const thread_guard&) = delete; thread_guard& operator=(const thread_guard&) = delete; }; void func(){ thread t([]{ cout << "Hello thread" <<endl ; }); thread_guard g(t); }

不管是何種狀況,當函數退出時,局部變量g調用其析構函數銷燬,從而可以保證join必定會被調用。

向線程傳遞參數

向線程調用的函數傳遞參數也是很簡單的,只須要在構造thread的實例時,依次傳入便可。例如:

void func(int *a,int n){} int buffer[10]; thread t(func,buffer,10); t.join();

須要注意的是,默認的會將傳遞的參數以拷貝的方式複製到線程空間,即便參數的類型是引用。例如:

void func(int a,const string& str); thread t(func,3,"hello");

func的第二個參數是string &,而傳入的是一個字符串字面量。該字面量以const char*類型傳入線程空間後,在線程的空間內轉換爲string

若是在線程中使用引用來更新對象時,就須要注意了。默認的是將對象拷貝到線程空間,其引用的是拷貝的線程空間的對象,而不是初始但願改變的對象。以下:

class _tagNode { public: int a; int b; }; void func(_tagNode &node) { node.a = 10; node.b = 20; } void f() { _tagNode node; thread t(func, node); t.join(); cout << node.a << endl ; cout << node.b << endl ; }

在線程內,將對象的字段a和b設置爲新的值,可是在線程調用結束後,這兩個字段的值並不會改變。這樣因爲引用的其實是局部變量node的一個拷貝,而不是node自己。在將對象傳入線程的時候,調用std::ref,將node的引用傳入線程,而不是一個拷貝。thread t(func,std::ref(node));

也可使用類的成員函數做爲線程函數,示例以下

class _tagNode{ public: void do_some_work(int a); }; _tagNode node; thread t(&_tagNode::do_some_work, &node,20);

上面建立的線程會調用node.do_some_work(20),第三個參數爲成員函數的第一個參數,以此類推。

轉移線程的全部權

thread是可移動的(movable)的,但不可複製(copyable)。能夠經過move來改變線程的全部權,靈活的決定線程在何時join或者detach。

thread t1(f1); thread t3(move(t1));

將線程從t1轉移給t3,這時候t1就再也不擁有線程的全部權,調用t1.joint1.detach會出現異常,要使用t3來管理線程。這也就意味着thread能夠做爲函數的返回類型,或者做爲參數傳遞給函數,可以更爲方便的管理線程。

線程的標識類型爲std::thread::id,有兩種方式得到到線程的id。

  • 經過thread的實例調用get_id()直接獲取
  • 在當前線程上調用this_thread::get_id()獲取

總結

本文主要介紹了C++11引入的標準多線程庫的一些基本操做。有如下內容:

  • 線程的建立
  • 線程的執行方式,join或者detach
  • 向線程函數傳遞參數,須要注意的是線程默認是以拷貝的方式傳遞參數的,當指望傳入一個引用時,要使用std::ref進行轉換
  • 線程是movable的,能夠在函數內部或者外部進行傳遞
  • 每一個線程都一個標識,能夠調用get_id獲取。


若是您以爲閱讀本文對您有幫助,請點一下「推薦」按鈕,您的「推薦」將是我最大的寫做動力!歡迎各位轉載,可是未經做者本人贊成,轉載文章以後必須在文章頁面明顯位置給出做者和原文鏈接,不然保留追究法律責任的權利。
相關文章
相關標籤/搜索