@@ -402,7 +402,7 @@ int n = 1;
402
402
std::thread t(f, 3, n);
403
403
```
404
404
405
- 线程对象 t 的构造没有问题,可以通过编译,但是这个 n 实际上并没有按引用传递,而是被复制了 。我们可以打印地址来验证我们的猜想。
405
+ 线程对象 t 的构造没有问题,可以通过编译,但是这个 n 实际上并没有按引用传递,而按值复制的 。我们可以打印地址来验证我们的猜想。
406
406
407
407
```cpp
408
408
void f(int, const int& a) { // a 并非引用了局部对象 n
@@ -755,7 +755,7 @@ int main(){
755
755
756
756
了解其实现,才能更好的使用它,同时也能解释其使用与学习中的各种问题。如:
757
757
758
- - 为什么默认复制 ?
758
+ - 为什么默认按值复制 ?
759
759
- 为什么需要 `std::ref` ?
760
760
- 如何支持只能移动的对象?
761
761
- 如何做到接受任意[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象?
@@ -868,6 +868,116 @@ int main(){
868
868
869
869
## C++20 ` std::jthread `
870
870
871
+ ` std::jthread ` 相比于 C++11 引入的 ` std::thread ` ,只是多了两个功能:
872
+
873
+ 1 . ** RAII 管理** :在析构时自动调用 ` join() ` 。
874
+
875
+ 2 . ** 线程停止功能** :线程的取消/停止。
876
+
877
+ ### 零开销原则
878
+
879
+ 我知道你肯定有疑问,为什么 C++20 不直接为 ` std::thread ` 增加这两个功能,而是创造一个新的线程类型呢?
880
+
881
+ 这就是 C++ 的设计哲学,*** 零开销原则*** :* 你不需要为你没有用到的(特性)付出额外的开销* 。
882
+
883
+ ` std::jthread ` 的通常实现就是单纯的保有 ` std::thread ` + [ ` std::stop_source ` ] ( https://zh.cppreference.com/w/cpp/thread/stop_source ) 这两个数据成员:
884
+
885
+ ``` cpp
886
+ thread _Impl;
887
+ stop_source _Ssource;
888
+ ```
889
+
890
+ [ MSVC STL] ( https://github.com/microsoft/STL/blob/23344e2/stl/inc/thread#L435-L436 ) 、[ libstdc++] ( https://github.com/gcc-mirror/gcc/blob/1a5e4dd/libstdc%2B%2B-v3/include/std/thread#L290-L291 ) 、[ libc++] ( https://github.com/llvm/llvm-project/blob/7162fd7/libcxx/include/__thread/jthread.h#L125-L126 ) 均是如此。
891
+
892
+ ` stop_source ` 通常占 8 字节,先前 ` std::thread ` 源码解析详细聊过其不同标准库对其保有的成员不同,简单来说也就是 64 位环境,大小为 16 或者 8。也就是 ` sizeof(std::jthread) ` 的值相比 ` std::thread ` 会多 8 ,为 ` 24 ` 或 ` 16 ` 。
893
+
894
+ 引入 ` std::jthread ` 符合* 零开销原则* ,它通过创建新的类型提供了更多的功能,而没有影响到原来 ` std::thread ` 的性能和内存占用。
895
+
896
+ ### 线程停止
897
+
898
+ 第一个功能很简单,不用赘述,我们直接聊这个所谓的“** 线程停止** ”就好。
899
+
900
+ 首先要明确,C++ 的 ` std::jthread ` 提供的线程停止功能并不同于常见的 POSIX 函数 [ ` pthread_cancel ` ] ( https://pubs.opengroup.org/onlinepubs/9699919799/ ) 。` pthread_cancel ` 是一种发送取消请求的函数,但并不是强制性的线程终止方式。目标线程的可取消性状态和类型决定了取消何时生效。当取消被执行时,进行清理和终止线程[ ^ 2 ] 。
901
+
902
+ ` std::jthread ` 所谓的线程停止只是一种基于用户代码的控制机制,而不是一种强制性的线程终止。使用 ` std::stop_source ` 和 [ ` std::stop_token ` ] ( https://zh.cppreference.com/w/cpp/thread/stop_token ) 提供了一种优雅地请求线程停止的方式,** 但实际上停止的决定和实现都由用户代码来完成** 。
903
+
904
+ ``` cpp
905
+ using namespace std ::literals::chrono_literals;
906
+
907
+ void f(std::stop_token stop_token, int value){
908
+ while (!stop_token.stop_requested()){ // 检查是否已经收到停止请求
909
+ std::cout << value++ << ' ' << std::flush;
910
+ std::this_thread::sleep_for(200ms);
911
+ }
912
+ std::cout << std::endl;
913
+ }
914
+
915
+ int main(){
916
+ std::jthread thread{ f, 1 }; // 打印 1..15 大约 3 秒
917
+ std::this_thread::sleep_for(3s);
918
+ // jthread 的析构函数调用 request_stop() 和 join()。
919
+ }
920
+ ```
921
+
922
+ `std::jthread` 提供了三个成员函数进行所谓的**线程停止**:
923
+
924
+ - `get_stop_source`:返回与 `jthread` 对象关联的 `std::stop_source`,允许从外部请求线程停止。
925
+
926
+ - `get_stop_token`:返回与 `jthread` 对象**停止状态**[^3]关联的 `std::stop_token`,允许检查是否有停止请求。
927
+
928
+ - `request_stop`:请求线程停止。
929
+
930
+ 上面这段代码并未出现这三个函数的任何一个调用,不过在 `jthread` 的析构函数中,会调用 `request_stop` 请求线程停止。
931
+
932
+ ```cpp
933
+ void _Try_cancel_and_join() noexcept {
934
+ if (_Impl.joinable()) {
935
+ _Ssource.request_stop();
936
+ _Impl.join();
937
+ }
938
+ }
939
+ ~jthread() {
940
+ _Try_cancel_and_join();
941
+ }
942
+ ```
943
+
944
+ 至于 ` std::jthread thread{ f, 1 }; ` 函数 f 的 ` std::stop_token ` 的形参是谁传递的?其实就是线程对象自己调用 ` get_token() ` 传递的 ,源码一眼便可发现:
945
+
946
+ ``` cpp
947
+ template <class _Fn , class... _ Args, enable_if_t<! is_same_v<remove_cvref_t<_Fn> , jthread>, int> = 0>
948
+ _ NODISCARD_CTOR_JTHREAD explicit jthread(_ Fn&& _ Fx, _ Args&&... _ Ax) {
949
+ if constexpr (is_invocable_v<decay_t<_ Fn>, stop_token, decay_t<_ Args>...>) {
950
+ _ Impl._ Start(_ STD forward<_ Fn>(_ Fx), _ Ssource.get_token(), _ STD forward<_ Args>(_ Ax)...);
951
+ } else {
952
+ _ Impl._ Start(_ STD forward<_ Fn>(_ Fx), _ STD forward<_ Args>(_ Ax)...);
953
+ }
954
+ }
955
+ ```
956
+
957
+ 也就是说虽然最初的那段代码看似什么都没调用,但是实际什么都调用了。这所谓的线程停止,其实简单来说,有点像外部给线程传递信号一样。
958
+
959
+ ---
960
+
961
+ **`std::stop_source`**:
962
+
963
+ - 这是一个可以发出停止请求的类型。当你调用 `stop_source` 的 `request_stop()` 方法时,它会设置内部的停止状态为“已请求停止”。
964
+ - 任何持有与这个 `stop_source` 关联的 `std::stop_token` 对象都能检查到这个停止请求。
965
+
966
+ **`std::stop_token`**:
967
+
968
+ - 这是一个可以检查停止请求的类型。线程内部可以定期检查 `stop_token` 是否收到了停止请求。
969
+ - 通过调用 `stop_token.stop_requested()`,线程可以检测到停止状态是否已被设置为“已请求停止”。
970
+
971
+ ### 总结
972
+
973
+ **零开销原则**应当很好理解。我们本节的难点只在于使用到了一些 MSVC STL 的源码实现来配合理解,其主要在于“线程停止”。线程停止设施你会感觉是一种类似与外部与线程进行某种信号通信的设施,`std::stop_source` 和 `std::stop_token` 都与线程对象关联,然后来管理函数到底如何执行。
974
+
975
+ 我们并没有举很多的例子,我们觉得这一个小例子所牵扯到的内容也就足够了,关键在于理解其设计与概念。
976
+
977
+ [^2]:注:通常需要线程执行的函数中有一些系统调用,设置取消点,线程会在那个调用中结束。
978
+
979
+ [^3]:注:“停止状态”指的是由 std::stop_source 和 std::stop_token 管理的一种标志,用于通知线程应该停止执行。这种机制不是强制性的终止线程,而是提供一种线程内外都能检查和响应的信号。
980
+
871
981
## 总结
872
982
873
983
本章节的内容围绕着:“使用线程”,也就是"**使用 `std::thread`**"展开, `std::thread` 是我们学习 C++ 并发支持库的重中之重,本章的内容在市面上并不少见,但是却是少有的准确与完善。即使你早已学习乃至使用 C++ 标准库进行多线程编程,我相信本章也一定可以让你收获良多。
0 commit comments