C++ std::jthread线程类用法详解(附带实例)
C++11 的 std::thread 类代表单线程执行且允许多个函数同时执行。然而,它有很大的不便之处:
C++20 提供了改善的线程类 std::jthread(来自可结合线程),当对象被销毁但可结合时将自动调用 join()。更进一步,此类型支持通过 std::stop_source/std::stop_token 来取消并且它的析构函数在线程 join 前也会请求其停止。
在本节中,你将学习如何使用 std::jthread 线程类。为了使用 std::jthread,C++ 程序中需要引入
1) 如果你想使线程对象在作用域外自动 join,则使用 std::jthread 而不是 std::thread。你仍然可使用 std::thread 类里的所有方法,比如调用 join() 来显式 join:
2) 如果你需要能取消线程执行,你应该如下这么做:
3) 如果你需要取消多个线程的工作,则可如下做:
4) 如果在 stop_source 的请求取消时,你需要执行部分代码,你可以使用 std::stop_token 对象创建的 std::stop_callback,当停止请求发送后,此回调函数将被调用(通过与 std::stop_token 关联的 std::stop_source 对象):
std::jthread 的公共接口与 std::thread 也十分类似。所有 std::thread 中的方法在 std::jthread 中也有。
然而,std::jthread在以下几个关键方面有所区别:
你可以像创建 std::thread 一样创建 std::jthread 对象。然而传递给 std::jthread 的可调用函数接收 std::stop_token 类型作为第一个参数。
当你需要取消线程执行时,典型的场景包括图形用户界面里用户交互可能取消正在进行的工作,也可以预见很多其他类似的场景。此类线程函数的调用如下:
函数线程必须周期性地检查 std::stop_token 对象的状态。stop_requested() 方法检查停止请求是否发送。停止请求来自 std::stop_source 对象。
如果多个 stop token 关联同一个 stop source,则停止请求对所有 stop token 可见。如果停止请求了,则它无法被撤回,且连续的停止请求没有任何意义。为了请求停止,你应该调用 request_stop() 方法。通过调用 stop_possible() 方法,你可以检查 std::stop_source 是否与停止状态关联及是否可停止请求。
如果在 stop source 请求停止时,你需要调用回调函数,那么你可以使用 std::stop_callback 类。它将 std::stop_token 对象与回调函数绑定。当 stop token 的 stop source 请求停止时,回调函数被调用。回调函数调用如下:
你可以给同一 stop token 创建任意数量的 std::stop_callback 对象。然而,这些回调函数的调用顺序是不一定的。唯一保证的是在 std::stop_callback 对象创建后,请求停止,它们将会同步执行。
另外重要的一点是,如果任意回调函数返回异常,std::terminate() 会被调用。
- 你必须显式调用 join() 方法来等待线程执行完成;
- 如果 std::thread 对象被销毁了但仍可结合,则 std::terminate() 被调用,这将导致一些问题。
C++20 提供了改善的线程类 std::jthread(来自可结合线程),当对象被销毁但可结合时将自动调用 join()。更进一步,此类型支持通过 std::stop_source/std::stop_token 来取消并且它的析构函数在线程 join 前也会请求其停止。
在本节中,你将学习如何使用 std::jthread 线程类。为了使用 std::jthread,C++ 程序中需要引入
<thread>
头文件。对于 std::stop_source和std::stop_token,你需要引入头文件 <stop_token>
。C++ std::jthread的使用方式
典型的使用可结合线程和相应取消机制的场景如下:1) 如果你想使线程对象在作用域外自动 join,则使用 std::jthread 而不是 std::thread。你仍然可使用 std::thread 类里的所有方法,比如调用 join() 来显式 join:
void thread_func(int i) { while(i-- > 0) { std::cout << i << '\n'; } } int main() { std::jthread t(thread_func, 10); }
2) 如果你需要能取消线程执行,你应该如下这么做:
- 确保线程函数的第一个参数是 std::stop_token 对象;
- 在线程函数中,周期性地检查停止信号是否被 std::stop_token 对象通过 stop_requested() 请求,如果被请求则停止;
- 使用 std::jthread 在另外的线程上执行函数;
- 在调用线程里,使用 std::jthread 对象的 request_stop() 方法来请求线程函数停止并返回。
> void thread_func(std::stop_token st, int& i) { while(!st.stop_requested() && i < 100) { using namespace std::chrono_literals; std::this_thread::sleep_for(200ms); i++; } } int main() { int a = 0; std::jthread t(thread_func, std::ref(a)); using namespace std::chrono_literals; std::this_thread::sleep_for(1s); t.request_stop(); std::cout << a << '\n'; // prints 4 }
3) 如果你需要取消多个线程的工作,则可如下做:
- 所有线程函数必须接收 std::stop_token 对象作为第一个参数。
- 所有线程函数应该周期性检查停止信号是否被 std::stop_token 对象通过调用 stop_requested() 方法请求,如果被请求则停止执行。
- 使用 std::jthread 在不同的线程上执行函数。
- 在调用线程里,创建 std::stop_source 对象。
- 在 std::stop_source 对象上通过调用 get_token() 方法获取 std::stop_token 对象,在创建 std::jthread 对象时,将其作为第一个参数传递给线程函数。
- 当你需要停止线程函数的执行时,调用 std::stop_source 对象上的 request_stop() 方法。
> void thread_func(std::stop_token st, int& i) { while(!st.stop_requested() && i < 100) { using namespace std::chrono_literals; std::this_thread::sleep_for(200ms); i++; } } int main() { int a = 0; int b = 10; std::stop_source st; std::jthread t1(thread_func, st.get_token(), std::ref(a)); std::jthread t2(thread_func, st.get_token(), std::ref(b)); using namespace std::chrono_literals; std::this_thread::sleep_for(1s); st.request_stop(); std::cout << a << ' ' ' << b << '\n'; // prints 4 // and 14 }
4) 如果在 stop_source 的请求取消时,你需要执行部分代码,你可以使用 std::stop_token 对象创建的 std::stop_callback,当停止请求发送后,此回调函数将被调用(通过与 std::stop_token 关联的 std::stop_source 对象):
void thread_func(std::stop_token st, int& i) { while(!st.stop_requested() && i < 100) { using namespace std::chrono_literals; std::this_thread::sleep_for(200ms); i++; } } int main() { int a = 0; std::stop_source src; std::stop_token token = src.get_token(); std::stop_callback cb(token, []{std::cout << "the end\n";}); std::jthread t(thread_func, token, std::ref(a)); using namespace std::chrono_literals; std::this_thread::sleep_for(1s); src.request_stop(); std::cout << a << '\n'; // prints "the end" and 4 }
深度剖析C++ std::jthread
std::jthread 跟 std::thread 十分类似。实际上,它尝试修复 C++11 线程中的问题。std::jthread 的公共接口与 std::thread 也十分类似。所有 std::thread 中的方法在 std::jthread 中也有。
然而,std::jthread在以下几个关键方面有所区别:
- 在内部,至少从逻辑上讲它维护了共享停止状态,允许请求线程函数停止执行;
- 它有几个方法用来处理相关联的取消:get_stop_source(),返回线程共享停止状态的std::stop_source对象;get_stop_token(),返回线程共享停止状态的 std::stop_token 对象;request_stop(),通过共享停止状态请求取消线程函数的执行;
- 它的析构函数的执行是:当线程 joinable,调用 request_stop() 再调用 join() 来请求停止执行,然后等待线程结束执行。
你可以像创建 std::thread 一样创建 std::jthread 对象。然而传递给 std::jthread 的可调用函数接收 std::stop_token 类型作为第一个参数。
当你需要取消线程执行时,典型的场景包括图形用户界面里用户交互可能取消正在进行的工作,也可以预见很多其他类似的场景。此类线程函数的调用如下:
- 当构造 std::jthread 时,线程函数的第一个参数是 std::stop_token,它将传递给可调用函数;
- 如果可调用函数的第一个参数不是 std::stop_token 对象,则 std::stop_token 对象关联的 std::jthread 对象内部共享停止状态被传递给这个函数。此 token 可通过调用 get_stop_token() 获取。
函数线程必须周期性地检查 std::stop_token 对象的状态。stop_requested() 方法检查停止请求是否发送。停止请求来自 std::stop_source 对象。
如果多个 stop token 关联同一个 stop source,则停止请求对所有 stop token 可见。如果停止请求了,则它无法被撤回,且连续的停止请求没有任何意义。为了请求停止,你应该调用 request_stop() 方法。通过调用 stop_possible() 方法,你可以检查 std::stop_source 是否与停止状态关联及是否可停止请求。
如果在 stop source 请求停止时,你需要调用回调函数,那么你可以使用 std::stop_callback 类。它将 std::stop_token 对象与回调函数绑定。当 stop token 的 stop source 请求停止时,回调函数被调用。回调函数调用如下:
- 在同一线程中调用 request_stop();
- 在构造 std::stop_callback 对象的线程中,在stop callback 对象被构造前,停止请求已经被发送。
你可以给同一 stop token 创建任意数量的 std::stop_callback 对象。然而,这些回调函数的调用顺序是不一定的。唯一保证的是在 std::stop_callback 对象创建后,请求停止,它们将会同步执行。
另外重要的一点是,如果任意回调函数返回异常,std::terminate() 会被调用。