C++ std::thread 和 std::jthread 使用详解 (含C++20新特性)
目錄
std::thread
std::thread 構造函數?
觀察器
操作
std::jthread
std::jthread 構造函數
觀察器
操作
停止記號處理
管理當前線程的函數
yield()
get_id()
sleep_for()
sleep_until()
在C++ 11之前,官方并沒有支持線程庫。在Linux下完成多線程編程時,多數情況下是使用#include <pthread.h>的函數。C++ 11通過標準庫引入了對?thread 類的支持,大大方便了完成多線程開發的工作。在C++20中,引入的 jthread 類是 thread 自動合并和取消的實現版本。接下來將先從線程函數和 thread 類開始介紹,分析它們的不同,然后再介紹 jthread。
?
?
std::thread
?
std::thread 構造函數?
(1)thread()?noexcept; (2)thread(?thread&&?other?)?noexcept; (3)template<?class?Function,?class...?Args?>?explicit?thread(?Function&&?f, Args&&...?args?); (4)thread(const?thread&)?=?delete;(1)?構造新的?thread?對象,但由于沒有傳入函數,所以thread對象還沒有關聯到線程。
(2)?移動構造函數。構造表示曾為?other?所表示的執行線程的?thread?對象。此調用后?other?不再表示執行線程。
(3)?構造新的?std::thread?對象并將它與執行線程關聯。新的執行線程開始執行。
(4)?復制構造函數被刪除,?thread?不可復制。
#include <iostream> #include <utility> #include <thread> #include <chrono>void f1(int n) {for (int i = 0; i < 5; ++i) {std::cout << "Thread 1 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));} }void f2(int& n) {for (int i = 0; i < 5; ++i) {std::cout << "Thread 2 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));} }class foo { public:void bar(){for (int i = 0; i < 5; ++i) {std::cout << "Thread 3 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));}}int n = 0; };class baz { public:void operator()(){for (int i = 0; i < 5; ++i) {std::cout << "Thread 4 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));}}int n = 0; };int main() {int n = 0;foo f;baz b;std::thread t1; // t1 不是線程std::thread t2(f1, n + 1); // 按值傳遞std::thread t3(f2, std::ref(n)); // 按引用傳遞std::thread t4(std::move(t3)); // t4 現在運行 f2() 。 t3 不再是線程std::thread t5(&foo::bar, &f); // t5 在對象 f 上運行 foo::bar()std::thread t6(std::ref(b)); // t6 在對象 b 上運行 baz::operator()t2.join();t4.join();t5.join();t6.join();std::cout << "Final value of n is " << n << '\n';std::cout << "Final value of foo::n is " << f.n << '\n';std::cout << "Final value of baz::n is " << b.n << '\n'; }注意:若需要傳遞引用參數給線程函數,則必須包裝它 (例如用 std::ref?或?std::cref)。忽略來自函數的任何返回值。若函數拋異常,則調用?std::terminate。
?
pthread_create 線程創建函數
int?pthread_create?(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);功能:創建一個具有指定參數的線程。
形參:thread 是要創建的線程的線程ID指針。
pthread_t 類型的定義是 typedef unsigned long int pthread_t;(打印時要使用%lu或%u方式)。
attr:創建線程時的線程屬性(設置NULL表示使用默認線程屬性)。
start_routine:指向的是新線程將運行的函數。線程一旦被創建好,內核就可以調度內核線程來執行 start_routine 函數指針所指向的函數了。
arg:指向的是運行函數的形參。
返回值:若是成功建立線程返回0,否則返回錯誤的編號。
相對而言,使用線程創建函數更為復雜。但這并不意味這就不使用 pthread 提供的函數了,因為 pthread 中的函數可以設置線程的屬性。這在某些情況下是非常有用的,所以有時會將兩者結合起來使用。
?
?
觀察器
joinable
bool?joinable()?const?noexcept;用于判斷?thread?對象是否關聯到某一線程,若 thread 對象與執行線程關聯,則返回?true?,反之為?false?。
#include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::thread t;std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()<< '\n';t = std::thread(foo);std::cout << "after starting, joinable: " << t.joinable() << '\n';t.join();std::cout << "after joining, joinable: " << t.joinable() << '\n'; }before starting, joinable: false after starting, joinable: true after joining, joinable: false?
get_id
std::thread::id?get_id()?const?noexcept;返回標識與?*this?關聯的線程的 std::thread::id?。
#include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::thread t1(foo);std::thread::id t1_id = t1.get_id();std::thread t2(foo);std::thread::id t2_id = t2.get_id();std::cout << "t1's id: " << t1_id << '\n';std::cout << "t2's id: " << t2_id << '\n';t1.join();t2.join(); }t1's id: 0x35a7210f t2's id: 0x35a311c4?
?
操作
join
void?join();阻塞當前線程直至?*this?所標識的線程結束其執行。*this?所標識的線程的完成同步于對應的從?join()?成功返回。*this?自身上不進行同步。同時從多個線程在同一 thread 對象上調用?join()?構成數據競爭,導致未定義行為。
#include <iostream> #include <thread> #include <chrono>void foo() {// 模擬昂貴操作std::this_thread::sleep_for(std::chrono::seconds(1)); }void bar() {// 模擬昂貴操作std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::cout << "starting first helper...\n";std::thread helper1(foo);std::cout << "starting second helper...\n";std::thread helper2(bar);std::cout << "waiting for helpers to finish..." << std::endl;helper1.join();helper2.join();std::cout << "done!\n"; }starting first helper... starting second helper... waiting for helpers to finish... done!?
pthread_join 等待線程結束函數
int?pthread_join(pthread_t thread, void **retval);功能:這個函數是一個線程阻塞的函數,調用它的函數將一直等待到被等待的線程結束為止,當函數返回時,被等待線程的資源被收回。
形參:thread是被等待的線程標識符。
retval:一個用戶定義的指針,它可以用來存儲被等待線程的返回值。
返回值:成功返回0,否則返回錯誤的編號。
錯誤碼:
①EDEADLK:可能引起死鎖,比如兩個線程互相針對對方調用pthread_join,或者線程對自身調用pthread_join。
②EINVAL:目標線程是不可回收的,或者已經有其他線程在回收目標線程。
③ESRCH:目標線程不存在。
?
detach
void?detach();從 thread 對象分離執行線程,允許執行獨立地持續。一旦該線程退出,則釋放任何分配的資源。調用?detach?后?*this?不再占有任何線程。
#include <iostream> #include <chrono> #include <thread>void independentThread() {std::cout << "Starting concurrent thread.\n";std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Exiting concurrent thread.\n"; }void threadCaller() {std::cout << "Starting thread caller.\n";std::thread t(independentThread);t.detach();std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Exiting thread caller.\n"; }int main() {threadCaller();std::this_thread::sleep_for(std::chrono::seconds(5)); }Starting thread caller. Starting concurrent thread. Exiting thread caller. Exiting concurrent thread.?
pthread_detach 分離釋放線程函數
int?pthread_detach?(pthread_t thread);功能:允許分離執行線程,即獨立的執行。也是線程資源釋放的一種方式,一旦線程退出,則釋放分配的資源。
形參:thread 是要釋放線程的標識符 Id。
返回值:若是成功返回 0,否則返回錯誤的編號。
其他說明:linux 線程執行和 windows 不同,pthread 有兩種狀態 joinable 狀態和 unjoinable 狀態。一個線程默認的狀態是 joinable,如果線程是 joinable 狀態,當線程函數自己返回退出時或 pthread_exit 時,都不會釋放線程所占用堆棧和線程描述符(總計8K多),只有當調用了 pthread_join 之后這些資源才會被釋放。若是 unjoinable 狀態的線程,這些資源在線程函數退出時或 pthread_exit 時自動會被釋放。unjoinable 屬性可以在 pthread_create 時指定,或在線程創建后在線程中 pthread_detach 自己設置,如:pthread_detach( pthread_self() ),將狀態改為 unjoinable 狀態,確保資源的釋放。如果線程狀態為 joinable ,需要在之后適時調用 pthread_join。
?
swap
void?swap(?std::thread&?other?)?noexcept;交換二個 thread 對象的底層柄。
#include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }void bar() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::thread t1(foo);std::thread t2(bar);std::cout << "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';std::swap(t1, t2);std::cout << "after std::swap(t1, t2):" << '\n'<< "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';t1.swap(t2);std::cout << "after t1.swap(t2):" << '\n'<< "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';t1.join();t2.join(); }thread 1 id: 140185268262656 thread 2 id: 140185259869952 after std::swap(t1, t2): thread 1 id: 140185259869952 thread 2 id: 140185268262656 after t1.swap(t2): thread 1 id: 140185268262656 thread 2 id: 140185259869952?
總結:
(0x01) std::thread 類創建線程非常方便,構造 thread 對象時傳入一個需要運行的函數及其參數。構造完成后,新的線程馬上被創建,同時執行該對象。注意:若需要傳遞引用參數給線程函數,則必須包裝它(例如用?std::ref?或?std::cref)。
(0x02) 使用 std::thread 默認的構造函數構造對象時,該對象是不關聯任何線程的。可以在之后的使用過程中再關聯到某一線程??梢酝ㄟ^使用 joinable() 接口,判斷一個 thread 對象是否關聯某個線程。
(0x03)?關聯到線程的 thread 對象析構前,必須調用 join() 接口等待線程結束。或者 thread 對象調用 detach() 接口解除與線程的關聯,否則會拋異常。
(0x04) thread 對象 detach() 后會獨立執行直至結束,而對應的 thread 對象變成不關聯任何線程的對象,joinable() 將返回 false。
(0x05) std::thread 沒有拷貝構造函數和拷貝賦值操作符,因此不支持復制操作(但從構造函數的示例代碼可以看出 std::thread 可以 move )。這也說明了沒有兩個 thread 對象可以表示同一執行線程。
?
?
std::jthread
它擁有同 std::thread?的行為外,主要增加了以下兩個功能:
(1)?std::jthread 對象被 destruct 時,會自動調用 join,等待其所表示的執行流結束。
(2)?支持外部請求中止(通過 get_stop_source、get_stop_token 和 request_stop )。
?
為什么不是選擇往 std::thread 添加新接口,而是引入了一個新的標準庫?
因為 std::jthread 為了實現上述新功能,帶來了額外的性能開銷 (主要是多了一個成員變量)。而根據 C++ 一直以來?"不為不使用的功能付費"?的設計哲學,他們自然就把這些新功能拆出來新做了一個類。
?
std::jthread 構造函數
(1)jthread()?noexcept; (2)jthread(?jthread&&?other?)?noexcept; (3)template<?class?Function,?class...?Args?>?explicit?jthread(?Function&&?f, Args&&...?args?); (4)jthread(?const?jthread&?)?=?delete;(1)?構造新的?jthread?對象,但由于沒有傳入函數,所以 jthread 對象還沒有關聯到線程。
(2)?移動構造函數。構造的?jthread?對象表示之前由?other?表示的執行線程。此調用后?other?不再表示執行線程。
(3)?創建與執行線程關聯的新?std::jthread?對象。若函數?f?接受?std::stop_token?作為其首參數,則新線程開始執行。
(4)?復制構造函數被刪除;線程不可復制。
#include <iostream> #include <utility> #include <thread> #include <chrono>void f1(int n) {for (int i = 0; i < 5; ++i) {std::cout << "Thread 1 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));} }void f2(int& n) {for (int i = 0; i < 5; ++i) {std::cout << "Thread 2 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));} }class foo { public:void bar(){for (int i = 0; i < 5; ++i) {std::cout << "Thread 3 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));}}int n = 0; };class baz { public:void operator()(){for (int i = 0; i < 5; ++i) {std::cout << "Thread 4 executing\n";++n;std::this_thread::sleep_for(std::chrono::milliseconds(10));}}int n = 0; };int main() {int n = 0;foo f;baz b;std::jthread t0; // t0 不是線程std::jthread t1(f1, n + 1); // 按值傳遞std::jthread t2a(f2, std::ref(n)); // 按引用傳遞std::jthread t2b(std::move(t2a)); // t2b 現在運行 f2() 。 t2a 不再是線程std::jthread t3(&foo::bar, &f); // t3 在對象 f 上運行 foo::bar()std::jthread t4(b); // t4 在對象 b 上運行 baz::operator()t1.join();t2b.join();t3.join();std::cout << "Final value of n is " << n << '\n';std::cout << "Final value of foo::n is " << f.n << '\n';// t4 在析構時結合 }?
?
觀察器
joinable
[[nodiscard]]?bool?joinable()?const?noexcept; #include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::jthread t;std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()<< '\n';t = std::thread(foo);std::cout << "after starting, joinable: " << t.joinable() << '\n';t.join();std::cout << "after joining, joinable: " << t.joinable() << '\n'; }before starting, joinable: false after starting, joinable: true after joining, joinable: false?
get_id
[[nodiscard]]?std::jthread::id?get_id()?const?noexcept;返回標識與?*this?關聯的線程的?std::jthread::id?。
#include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::jthread t1(foo);std::jthread::id t1_id = t1.get_id();std::jthread t2(foo);std::jthread::id t2_id = t2.get_id();std::cout << "t1's id: " << t1_id << '\n';std::cout << "t2's id: " << t2_id << '\n';}t1's id: 0x35a7210f t2's id: 0x35a311c4?
?
操作
join
void?join();阻塞當前線程直至?*this?所標識的線程結束其執行。*this?所標識的線程的完成同步于對應的從?join()?成功返回。*this?自身上不進行同步。同時從多個線程在同一 jthread 對象上調用?join()?構成數據競爭,導致未定義行為。
#include <iostream> #include <thread> #include <chrono>void foo() {// 模擬昂貴操作std::this_thread::sleep_for(std::chrono::seconds(1)); }void bar() {// 模擬昂貴操作std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::cout << "starting first helper...\n";std::jthread helper1(foo);std::cout << "starting second helper...\n";std::jthread helper2(bar);std::cout << "waiting for helpers to finish..." << std::endl;helper1.join();helper2.join();std::cout << "done!\n"; }starting first helper... starting second helper... waiting for helpers to finish... done!?
detach
void?detach();從 jthread 對象分離執行線程,允許線程獨立地運行。一旦該線程退出,則釋放任何分配的資源。調用?detach?后?*this?不再占有任何線程。
#include <iostream> #include <chrono> #include <thread>void independentThread() {std::cout << "Starting concurrent thread.\n";std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "Exiting concurrent thread.\n"; }void threadCaller() {std::cout << "Starting thread caller.\n";std::jthread t(independentThread);t.detach();std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "Exiting thread caller.\n"; }int main() {threadCaller();std::this_thread::sleep_for(std::chrono::seconds(5)); }Starting thread caller. Starting concurrent thread. Exiting thread caller. Exiting concurrent thread.?
swap
void?swap(?std::jthread&?other?)?noexcept;交換二個 jthread 對象的底層柄。
#include <iostream> #include <thread> #include <chrono>void foo() {std::this_thread::sleep_for(std::chrono::seconds(1)); }void bar() {std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::jthread t1(foo);std::jthread t2(bar);std::cout << "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';std::swap(t1, t2);std::cout << "after std::swap(t1, t2):" << '\n'<< "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';t1.swap(t2);std::cout << "after t1.swap(t2):" << '\n'<< "thread 1 id: " << t1.get_id() << '\n'<< "thread 2 id: " << t2.get_id() << '\n';}thread 1 id: 140185268262656 thread 2 id: 140185259869952 after std::swap(t1, t2): thread 1 id: 140185259869952 thread 2 id: 140185268262656 after t1.swap(t2): thread 1 id: 140185268262656 thread 2 id: 140185259869952?
?
停止記號處理
get_stop_source
std::stop_source?get_stop_source()?const?noexcept;返回?std::stop_source?,擁有與?jthread?對象內部所保有者相同的共享停止狀態。
?
get_stop_token
std::stop_token?get_stop_token()?const?noexcept;返回?std::stop_token?,與?jthread?對象內部保有的同一共享停止狀態關聯。
?
request_stop
bool?request_stop()?noexcept;若內部停止狀態尚未被請求停止,則對它發出停止請求。原子地作出確定,而若請求了停止,則原子地更新共享狀態以避免競爭條件,使得:能在同一共享狀態的?std::stop_token?與?std::stop_source?上同時調用?stop_requested()?與?stop_possible() 能從多個線程在同一?jthread?對象或與同一停止狀態關聯的其他?std::stop_source?對象上并發調用?request_stop()?,而將只有一個線程實際進行停止請求。
注意:若?request_stop()?發出停止請求(即返回?true?),則將在發出?request_stop()?的同一線程上同步調用對同一共享停止狀態注冊的任何?std::stop_callbacks?。若任何回調的調用經由異常退出,則調用?std::terminate?。
若已作出停止請求,則此函數返回?false?。然而不保證正好對同一停止狀態(成功)請求停止的另一線程或?std::stop_source?對象不仍然在調用?std::stop_callback?函數的中間。
若?request_stop()?發出停止請求(即返回?true?),則提醒所有用與?jthread?的內部停止狀態關聯的?stop_token?的可中斷等待注冊的、基類型為?std::condition_variable_any?的條件變量。
?
?
管理當前線程的函數
yield()
void?yield()?noexcept;提供提示給實現,以重調度線程的執行,允許其他線程運行。
#include <iostream> #include <chrono> #include <thread>// 建議其他線程運行一小段時間的“忙睡眠” void little_sleep(std::chrono::microseconds us) {auto start = std::chrono::high_resolution_clock::now();auto end = start + us;do {std::this_thread::yield();} while (std::chrono::high_resolution_clock::now() < end); }int main() {auto start = std::chrono::high_resolution_clock::now();little_sleep(std::chrono::microseconds(100));auto elapsed = std::chrono::high_resolution_clock::now() - start;std::cout << "waited for "<< std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()<< " microseconds\n"; }waited for 128 microseconds?
get_id()
std::thread::id?get_id()?noexcept; #include <iostream> #include <thread> #include <chrono> #include <mutex>std::mutex g_display_mutex;void foo() {std::thread::id this_id = std::this_thread::get_id();g_display_mutex.lock();std::cout << "thread " << this_id << " sleeping...\n";g_display_mutex.unlock();std::this_thread::sleep_for(std::chrono::seconds(1)); }int main() {std::thread t1(foo);std::thread t2(foo);t1.join();t2.join(); }thread 0x2384b312 sleeping... thread 0x228a10fc sleeping...?
sleep_for()
template<?class?Rep,?class?Period?> void?sleep_for(?const?std::chrono::duration<Rep, Period>&?sleep_duration?);阻塞當前線程執行,至少經過指定的?sleep_duration?。此函數可能阻塞長于?sleep_duration?,因為調度或資源爭議延遲。標準庫建議用穩定時鐘度量時長。若實現用系統時間代替,則等待時間亦可能對時鐘調節敏感。
#include <iostream> #include <chrono> #include <thread>int main() {using namespace std::chrono_literals; // C++14std::cout << "Hello waiter" << std::endl; // 有意沖入auto start = std::chrono::high_resolution_clock::now();std::this_thread::sleep_for(2s);auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double, std::milli> elapsed = end-start;std::cout << "Waited " << elapsed.count() << " ms\n"; }Hello waiter Waited 2000.12 ms?
sleep_until()
template<?class?Clock,?class?Duration?> void?sleep_until(?const?std::chrono::time_point<Clock,Duration>&?sleep_time?);阻塞當前線程,直至抵達指定的?sleep_time?。使用聯傾向于?sleep_time?的時鐘,這表示時鐘調節有影響。從而在調用時間點后,阻塞的時長可能小于,但不會多于?sleep_time?-?Clock::now()?。函數亦可能阻塞長于抵達?sleep_time?之后,由于調度或資源爭議延遲。
?
?
參考:https://zh.cppreference.com/w/cpp/thread
https://www.zhihu.com/question/364140779
https://blog.csdn.net/qq_38289815/article/details/82950320
總結
以上是生活随笔為你收集整理的C++ std::thread 和 std::jthread 使用详解 (含C++20新特性)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ priority_queue 的
- 下一篇: 图文并茂的讲解 ICMP (网际控制报文