C++20 新的 thread:jthread

| | 0 Comments| 10:12
Categories:

很久以前,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 是否有被要求停止。

在主執行序的部分,則是會在五秒後呼叫 t1request_stop()、要求他停止。在呼叫了這個函式後, lambda 內的 tokenstop_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;他的建構子的第一個引數是 t1stop_token、也就是之後拿來觸發事件用的物件,而第二個引數則是要執行的內容,這邊是一個 lambda。

之後,在呼叫 t1request_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 來實作,應該算是提供了一個更明確的標準架構了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *