未來芯片制造,如果突破不了 5nm 極限,則 CPU 性能的提升,可能會依賴於三維集成技術,將多個 CPU 核集成在一起,使得多核系統越來越普遍。
以前的 C++ 多線程,一是受限於平台,多借助於封裝好的 APIs 來完成,例如:POSIX threads,Windows threads 等;二是受限於單核系統,本質上都是“偽多線程”:通過線程調度,使得單核系統進行任務的切換,形成多線程的假象。
新的 C++11 標准,在語言層面上實現了多線程,其庫中提供了相關組件,使得跨平台編寫多線程 cpp 程序成為可能,最終能夠在多核系統中實現真正的並行計算。
1 並發 (concurrency)
1.1 並發與並行
計算機中的並發,指的是在單一系統中,同時執行多個獨立的活動。對於多核系統,它們在同一時刻進行的活動,則稱為並行。
通俗的理解:當有兩個人都在邊吃飯邊看電視時,對於一個人而言,吃飯和看電視就是並發,對於兩個人而言,他們在同一時刻進行的活動,可稱為並行。
1) 單核的任務切換 (task switching)
2) 雙核的並行執行 (parallel execution)
1.2 線程與進程
如果將進程比作一套房子,那么住在房子里的人,其從事的各種活動 (比如,廚房做飯,客廳吃飯,卧室看電視),就是一個個的線程。現在又搬來一個人,則當兩個人都在房子里,做着各自的活動時,程序便由以前的單線程變為了多線程。
有的房間 (比如客廳) 兩個人都可以同時進出,這代表着進程中某些內存空間是共享的,每個線程都可以使用這些共享內存。有的房間 (比如廁所) 一次只能容納一個人,先進去的把門鎖上,后到的人看到鎖,就在外面等待,直到先進去的人把鎖打開,這就是“互斥核” (mutex)
在應用程序中,具體利用到並發編程的,一是多進程並發,二是多線程並發,如下圖:
(1) 多進程並發 (2) 多線程並發
2 程序示例
實現多線程需要 1) 頭文件 <thread> 2) 單獨的線程函數 threadFunc() 3)線程對象 thread t(threadFunc) 4)等待線程 join()
#include <thread> #include <iostream> void threadFunc() { std::cout << "This is a thread!" << std::endl; } int main() { std::thread t(threadFunc); t.join(); std::cout << "This is main program!" << std::endl; return 0; }
輸出結果為:
This is a thread! This is main program!
當使用 t.detach() 來代替 t.join() 時,主線程 main 不會等待新線程 t(threadFunc),只會獨自運行到程序結束。
3 任務代替線程
3.1 兩個問題
1) 線程耗盡 (exhaustion)
軟件線程是一種有限的資源,當創建的線程數量多於系統所能夠提供的,一個異常 std::system_error 就會拋出,程序便會終止。
2) 線程超額 (oversubscription)
當等待運行的線程 (ready-to-run) 多於系統硬件線程 (hardware threads) 時,線程調度器會為每個軟件線程在硬件線程上分配時間片 (time-slice)。若一個線程的時間片結束,另一個線程的時間片剛開始時,上下文的切換 (context switch) 就會被執行。對於多核系統,當線程從一個 CPU 被切換到另一個 CPU 中時,會造成很大的資源消耗。
3.2 基於任務 (task-based)
基於線程編程 (thread-based),必須手動管理上面的兩個問題,增加了編程的難度。
基於任務編程 (task-based),通過 std::async,可將問題交由 C++標准庫處理,簡化了程序。
1) 頭文件 <future>
2) 單獨的任務函數 taskFunc1 和 taskFunc2
3) 異步任務對象 auto fut1 = std::async(taskFunc1)
4) 獲取任務函數返回值 fut1.get()
#include <iostream> #include <thread> #include <future> std::string taskFunc1() { std::string str = "This is task1"; return str; } std::string taskFunc2() { std::string str = " and task2"; return str; } int main() { auto fut1 = std::async(taskFunc1); auto fut2 = std::async(taskFunc2); std::cout << fut1.get() + fut2.get() << std::endl << "This is main program" << std::endl; return 0; }
輸出結果為:
This is task1 and task2 This is main program
小結:
1) thread-based programming needs manual management of thread exhaustion, oversubscription, load balancing, and adaptation to new platforms.
2) task-based programming handles most of these issues via std::async with the default launch policy
參考資料:
<C++ Concurrency in Action> chapter 1
<Effecctive Modern C++> chapter 7