如何使用 C++11 編寫 Linux 多線程程序
本文講述了如何使用 C++11 編寫 Linux 下的多線程程序,如何使用鎖,以及相關的注意事項,還簡述了 C++11 引入的一些高級概念如 promise/future 等。
在這個多核時代,如何充分利用每個 CPU 內核是一個繞不開的話題,從需要為成千上萬的用戶同時提供服務的服務端應用程序,到需要同時打開十幾個頁面,每個頁面都有幾十上百個鏈接的 web 浏覽器應用程序,從保持著幾 t 甚或幾 p 的數據的數據庫系統,到手機上的一個有良好用戶響應能力的 app,為了充分利用每個 CPU 內核,都會想到是否可以使用多線程技術。這裡所說的“充分利用”包含了兩個層面的意思,一個是使用到所有的內核,再一個是內核不空閒,不讓某個內核長時間處於空閒狀態。在 C++98 的時代,C++標准並沒有包含多線程的支持,人們只能直接調用操作系統提供的 SDK API 來編寫多線程程序,不同的操作系統提供的 SDK API 以及線程控制能力不盡相同,到了 C++11,終於在標准之中加入了正式的多線程的支持,從而我們可以使用標准形式的類來創建與執行線程,也使得我們可以使用標准形式的鎖、原子操作、線程本地存儲 (TLS) 等來進行復雜的各種模式的多線程編程,而且,C++11 還提供了一些高級概念,比如 promise/future,packaged_task,async 等以簡化某些模式的多線程編程。
多線程可以讓我們的應用程序擁有更加出色的性能,同時,如果沒有用好,多線程又是比較容易出錯的且難以查找錯誤所在,甚至可以讓人們覺得自己陷進了泥潭,希望本文能夠幫助您更好地使用 C++11 來進行 Linux 下的多線程編程。
首先我們應該正確地認識線程。維基百科對線程的定義是:線程是一個編排好的指令序列,這個指令序列(線程)可以和其它的指令序列(線程)並行執行,操作系統調度器將線程作為最小的 CPU 調度單元。在進行架構設計時,我們應該多從操作系統線程調度的角度去考慮應用程序的線程安排,而不僅僅是代碼。
當只有一個 CPU 內核可供調度時,多個線程的運行示意如下:
我們可以看到,這時的多線程本質上是單個 CPU 的時間分片,一個時間片運行一個線程的代碼,它可以支持並發處理,但是不能說是真正的並行計算。
當有多個 CPU 或者多個內核可供調度時,可以做到真正的並行計算,多個線程的運行示意如下:
從上述兩圖,我們可以直接得到使用多線程的一些常見場景:
需要注意一點,因為單個 CPU 內核下多個線程並不是真正的並行,有些問題,比如 CPU 緩存不一致問題,不一定能表現出來,一旦這些代碼被放到了多核或者多 CPU 的環境運行,就很可能會出現“在開發測試環境一切沒有問題,到了實施現場就莫名其妙”的情況,所以,在進行多線程開發時,開發與測試環境應該是多核或者多 CPU 的,以避免出現這類情況。
C++11新特性:Lambda函數(匿名函數) http://www.linuxidc.com/Linux/2013-12/93367p2.htm
C++ Primer Plus 第6版 中文版 清晰有書簽PDF+源代碼 http://www.linuxidc.com/Linux/2014-05/101227.htm
讀C++ Primer 之構造函數陷阱 http://www.linuxidc.com/Linux/2011-08/40176.htm
讀C++ Primer 之智能指針 http://www.linuxidc.com/Linux/2011-08/40177.htm
讀C++ Primer 之句柄類 http://www.linuxidc.com/Linux/2011-08/40175.htm
C++11 獲取系統時間庫函數 time since epoch http://www.linuxidc.com/Linux/2014-03/97446.htm
C++11中正則表達式測試 http://www.linuxidc.com/Linux/2012-08/69086.htm
C++11 的標准類 std::thread 對線程進行了封裝,它的聲明放在頭文件 thread 中,其中聲明了線程類 thread, 線程標識符 id,以及名字空間 this_thread,按照 C++11 規范,這個頭文件至少應該兼容如下內容:
namespace std{ struct thread{ // native_handle_type 是連接 thread 類和操作系統 SDK API 之間的橋梁。 typedef implementation-dependent native_handle_type; native_handle_type native_handle(); // struct id{ id() noexcept; // 可以由==, < 兩個運算衍生出其它大小關系運算。 bool operator==(thread::id x, thread::id y) noexcept; bool operator<(thread::id x, thread::id y) noexcept; template<class charT, class traits> basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits>&out, thread::id id); // 哈希函數 template <class T> struct hash; template <> struct hash<thread::id>; }; id get_id() const noexcept; // 構造與析構 thread() noexcept; template<class F, class… Args> explicit thread(F&f, Args&&… args); ~thread(); thread(const thread&) = delete; thread(thread&&) noexcept; thread& operator=( const thread&) = delete; thread& operator=(thread&&) noexcept; // void swap(thread&) noexcept; bool joinable() const noexcept; void join(); void detach(); // 獲取物理線程數目 static unsigned hardware_concurrency() noexcept; } namespace this_thead{ thread::id get_id(); void yield(); template<class Clock, class Duration> void sleep_until(const chrono::time_point<Clock, Duration>& abs_time); template<class Rep, class Period> void sleep_for(const chromo::duration<Rep, Period>& rel_time); } }
和有些語言中定義的線程不同,C++11 所定義的線程是和操作系的線程是一一對應的,也就是說我們生成的線程都是直接接受操作系統的調度的,通過操作系統的相關命令(比如 ps -M 命令)是可以看到的,一個進程所能創建的線程數目以及一個操作系統所能創建的總的線程數目等都由運行時操作系統限定。
native_handle_type 是連接 thread 類和操作系統 SDK API 之間的橋梁,在 g++(libstdc++) for Linux 裡面,native_handle_type 其實就是 pthread 裡面的 pthread_t 類型,當 thread 類的功能不能滿足我們的要求的時候(比如改變某個線程的優先級),可以通過 thread 類實例的 native_handle() 返回值作為參數來調用相關的 pthread 函數達到目的。thread::id 定義了在運行時操作系統內唯一能夠標識該線程的標識符,同時其值還能指示所標識的線程的狀態,其默認值 (thread::id()) 表示不存在可控的正在執行的線程(即空線程,比如,調用 thead() 生成的沒有指定入口函數的線程類實例),當一個線程類實例的 get_id() 等於默認值的時候,即 get_id() == thread::id(),表示這個線程類實例處於下述狀態之一:
空線程 id 字符串表示形式依具體實現而定,有些編譯器為 0x0,有些為一句語義解釋。
有時候我們需要在線程執行代碼裡面對當前調用者線程進行操作,針對這種情況,C++11 裡面專門定義了一個名字空間 this_thread,其中包括 get_id() 函數可用來獲取當前調用者線程的 id,yield() 函數可以用來將調用者線程跳出運行狀態,重新交給操作系統進行調度,sleep_until 和 sleep_for 函數則可以讓調用者線程休眠若干時間。get_id() 函數實際上是通過調用 pthread_self() 函數獲得調用者線程的標識符,而 yield() 函數則是通過調用操作系統 API sched_yield() 進行調度切換。
更多詳情見請繼續閱讀下一頁的精彩內容: http://www.linuxidc.com/Linux/2014-12/110391p2.htm