線程很像輕量級的進程,但是一個進程中的所有線程都共享相同的地址空間,線程間的大部分數據都可以共享。線程間的通信一般都通過共享內存來實現。
優點:
- 由於可以共享數據,多線程間的通信開銷比進程小的多。
- 線程啓動的比進程快,佔用的資源更少。
缺點:
- 共享數據太過於靈活,爲了維護正確的共享,代碼寫起來比較複雜。
- 無法部署在分佈式系統上。
爲什麼使用併發
主要原因有兩個:任務拆分和提高性能。
任務拆分
在編寫軟件的時候,將相關的代碼放在一起,將無關的代碼分開,這是一個好主意,這樣能夠讓程序更加容易理解和測試。將程序劃分成不同的任務,每個線程執行一個任務或者多個任務,可以將整個程序的邏輯變得更加簡單。
提高性能
在兩種情況下,併發能夠提高性能。
- 任務並行(task parallelism):將一個單個任務分成若干個部分各自並行運行,從而降低運行時間。雖然聽起來很簡單,但其實是一個相當複雜的過程,設想假如各個部分之間存在很多以來,一個部分的執行需要使用到另一個任務的執行結果,這個時候並不能很好的並行完成。
- 數據並行(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;
}
分析:
- 首先,構建一個
std::thread
對象t1
,構造的時候傳遞了一個參數,這個參數是一個函數,這個函數就是這個線程的入口函數,函數執行完了,整個線程也就執行完了。 - 線程創建成功後,就會立即啓動,並沒有一個類似
start
的函數來顯式的啓動線程。 - 一旦線程開始運行, 就需要顯式的決定是要等待它完成(join),或者分離它讓它自行運行(detach)。注意:只需要在
std::thread
對象被銷燬之前做出這個決定。這個例子中,對象t1
是棧上變量,在main
函數執行結束後就會被銷燬,所以需要在main
函數結束之前做決定。 - 這個例子中選擇了使用
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
分析:
- 由於線程入口函數內部有個
500ms
的延時,所以在還沒有打印的時候,test()
已經執行完成了,t1
已經被析構了,但是它負責的那個線程還是能夠運行,這就是detach()
的作用。 - 如果去掉
main
函數中的1s
延時,會發現什麼都沒有打印,因爲主線程執行的太快,整個程序已經結束了,那個後臺線程被C++
運行時庫回收了。 - 如果將
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());
}