很久以前,Heresy 有寫過幾篇文章來介紹 C++ 11 標準函式庫提供的 thread 這個撰寫多執行序程式的功能了。
而在 C++ 20 的時候,標準函式庫又加入了一個同性質、但是功能更多一點的 jthread(C++ Reference),提供了自動 join 和可停止的機制,這邊就來稍微玩看看吧~
自動 join
首先,本來的 std::thread
在使用時,一般都需要在最後、透過 join()
這個函式來確保新建立的執行序已經完成,這樣才不會出問題。
以下面的例子來說:
#include <iostream> #include <thread> using namespace std::chrono_literals; int main() { std::cout << "start" << std::endl; std::thread t1( []() { std::cout << "thread t1 start" << std::endl; std::this_thread::sleep_for(1s); std::cout << "thread t1 stop" << std::endl; }); std::cout << "wait thread t1" << std::endl; t1.join(); std::cout << "stop" << std::endl; }
如果把「t1.join();
」這行刪除的話,那就會出現在主程式已經結束、但是 t1
所建立的執行序還在執行的錯誤狀況。
而如果改用 std::jthread
的話,他會在物件本身的生命周期結束、在呼叫解構子的時候自動嘗試去 join 這個執行序,所以在使用上算是比較安全的設計。
如果要改用 jthread
的話,就只需要改一個地方:
#include <iostream> #include <thread> using namespace std::chrono_literals; int main() { std::cout << "start" << std::endl; std::jthread t1( []() { std::cout << "thread t1 start" << std::endl; std::this_thread::sleep_for(1s); std::cout << "thread t1 stop" << std::endl; }); std::cout << "stop" << std::endl; }
而之後,在程式結束、t1
的生命週期結束的時候,就會自動等他執行完了。
不過,以這個例子來說,執行結果可能會是下面的樣子:
start
stop
thread t1 start
thread t1 stop
這是由於 thread 在建立後其實還沒真的起來、主執行序就繼續跑了,結果變成先輸出了「stop」、t1
才跑起來。
而如果想要進一步控制 t1
、確保它在輸出「stop」前就結束的話,除了一樣可以手動呼叫 join()
外,也可以透過 {}
來控制他的生命週期,例如:
#include <iostream> #include <thread> using namespace std::chrono_literals; int main() { std::cout << "start" << std::endl; { std::jthread t1( []() { std::cout << "thread t1 start" << std::endl; std::this_thread::sleep_for(1s); std::cout << "thread t1 stop" << std::endl; }); } std::cout << "stop" << std::endl; }
這樣執行的結果就會是:
start
thread t1 start
thread t1 stop
stop
這樣應該會更符合直覺,但是基本上還是看需要來寫了。
中斷執行序
std::jthread
除了加入了自動 join 的機制外,他還加入明確的停止/取消的機制。
他基本上是透過 std::stop_token
、來告訴建立出來的執行序是否有被要求中斷;不過當然啦,要執行的內容還是得自己去偵測、中斷才行。
下面就是一個簡單的例子:
#include <iostream> #include <chrono> #include <thread> using namespace std::chrono_literals; int main() { std::jthread t1( [](std::stop_token token) { std::cout << "thread t1 start" << std::endl; while (!token.stop_requested()) { std::cout << std::chrono::system_clock::now() << std::endl; std::this_thread::sleep_for(1s); } std::cout << "thread t1 stop" << std::endl; }); std::this_thread::sleep_for(5s); t1.request_stop(); std::cout << "stop" << std::endl; }
在這個例子中,用來建立 jthread
的 lambda 函式多了一個型別是 std::stop_token
的引數 token
;然後它的內容,實際上一個無限迴圈、每秒鐘會輸出一次現在的時間,不過每次執行的時候,都會去檢查 token
是否有被要求停止。
在主執行序的部分,則是會在五秒後呼叫 t1
的 request_stop()
、要求他停止。在呼叫了這個函式後, lambda 內的 token
的 stop_requested()
就會回傳 true
、讓無窮迴圈中斷、進而讓整個執行序結束。
透過這樣的機制,就可以比較簡單地實作 thread 的取消了。
當然啦,這邊也還是需要執行序裡面要執行的東西可以、有辦法中斷才行。比如說是去呼叫第三方函示庫的功能、然後他不支援中斷的功能的話,其實也是沒辦法玩的;像是某些存取檔案格式的函式庫,其實似乎就沒辦法很好地中斷。
std::stop_source
除了上面用的方法外,jthread
還提供了一個 std::stop_source
(參考)、可以用來送出停止的要求。
像是上面的「t1.request_stop();
」也可以改成下面的樣子:
std::stop_source ss = t1.get_stop_source(); ss.request_stop();
這個 stop_source
基本上就變成一個用來要求 jthread
停止的物件,也可以傳遞給其他的執行序來做控制。
而 stop_source
也可以用來傳遞給其他執行序、用來同時中斷多個執行序。
像是下面就是先定義一個 stop_source
的變數 ss
、 然後建立四個 jthread
、把 ss
傳遞進去後、用來確認是否有被要求停止。
#include <iostream> #include <array> #include <chrono> #include <thread> #include <syncstream> using namespace std::chrono_literals; int main() { std::stop_source ss; std::array<std::jthread, 4> aThreads; int idx = 0; for (auto& rThread : aThreads) { rThread = std::jthread([](int idx, std::stop_source ss) { std::osyncstream(std::cout) << "thread " << idx << " start" << std::endl; while (!ss.stop_requested()) { std::osyncstream(std::cout) << "thread " << idx << " : " << std::chrono::system_clock::now() << std::endl; std::this_thread::sleep_for(1s); } std::osyncstream(std::cout) << "thread " << idx << " stop" << std::endl; }, idx++, ss); } std::this_thread::sleep_for(5s); ss.request_stop(); std::cout << "stop" << std::endl; }
如此一來,當呼叫「ss.request_stop();
」的時候,前面建立的四個執行序就可以同時中斷了。
停止時的 callback
標準函式庫在這邊也還有提供一個 std::stop_callback
(參考)、可以用來監控 stop_token
,當 stop_token
被要求停止的時候、就會去執行制定的內容。
下面就是個簡單的例子:
#include <iostream> #include <chrono> #include <thread> using namespace std::chrono_literals; int main() { std::jthread t1( [](std::stop_token token) { std::cout << "thread t1 start" << std::endl; while (!token.stop_requested()) { std::cout << std::chrono::system_clock::now() << std::endl; std::this_thread::sleep_for(0.3s); } std::cout << "thread t1 stop" << std::endl; }); std::stop_callback sc(t1.get_stop_token(), []() { std::cout << "t1 is requested to stop" << std::endl; }); std::this_thread::sleep_for(1s); std::cout << "request stop" << std::endl; t1.request_stop(); std::cout << "stop" << std::endl; }
這邊在建立了 t1
這個 jthread
後,就建立一個 stop_callback
的物件 sc
;他的建構子的第一個引數是 t1
的 stop_token
、也就是之後拿來觸發事件用的物件,而第二個引數則是要執行的內容,這邊是一個 lambda。
之後,在呼叫 t1
的 request_stop()
的時候,除了會讓 t1
結束外,也會去執行這邊指定的內容。
以上面的程式來說,執行的結果應該會類似:
thread t1 start
2023-12-06 01:08:38.4462875
2023-12-06 01:08:38.7577195
2023-12-06 01:08:39.0704143
2023-12-06 01:08:39.3814970
request stop
t1 is requested to stop
stop
thread t1 stop
不過要注意的,是這個東西只有在有呼叫 request_stop()
的時候才會去執行,如果是 thread 自然結束的話,是不會觸發事件的。
這篇大概就先這樣了?其實整個看下來,jthread
的自動 join 主要是提供了開發上的安全性、可以避免撰寫程式的時候忘了去等執行序結束的狀況。
而要寫出可取消的執行序,其實在之前的標準其實也是可以透過 atomic<bool>
之類的自定義變數來做到一定程度;但是透過 request_stop()
和 stop_token
來實作,應該算是提供了一個更明確的標準架構了。