Skip to content

Commit 7453f85

Browse files
committed
1. 考虑用词”复制“改为”按值复制“,更加清晰
2. 修改第五章笔误 3. 完成第二章的 C++20 `std::jthread` 的全部内容 #12
1 parent 882aa86 commit 7453f85

File tree

3 files changed

+114
-4
lines changed

3 files changed

+114
-4
lines changed

md/02使用线程.md

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ int n = 1;
402402
std::thread t(f, 3, n);
403403
```
404404
405-
线程对象 t 的构造没有问题,可以通过编译,但是这个 n 实际上并没有按引用传递,而是被复制了。我们可以打印地址来验证我们的猜想。
405+
线程对象 t 的构造没有问题,可以通过编译,但是这个 n 实际上并没有按引用传递,而按值复制的。我们可以打印地址来验证我们的猜想。
406406
407407
```cpp
408408
void f(int, const int& a) { // a 并非引用了局部对象 n
@@ -755,7 +755,7 @@ int main(){
755755
756756
了解其实现,才能更好的使用它,同时也能解释其使用与学习中的各种问题。如:
757757
758-
- 为什么默认复制
758+
- 为什么默认按值复制
759759
- 为什么需要 `std::ref` ?
760760
- 如何支持只能移动的对象?
761761
- 如何做到接受任意[可调用](https://zh.cppreference.com/w/cpp/named_req/Callable)对象?
@@ -868,6 +868,116 @@ int main(){
868868
869869
## C++20 `std::jthread`
870870
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+
871981
## 总结
872982
873983
本章节的内容围绕着:“使用线程”,也就是"**使用 `std::thread`**"展开, `std::thread` 是我们学习 C++ 并发支持库的重中之重,本章的内容在市面上并不少见,但是却是少有的准确与完善。即使你早已学习乃至使用 C++ 标准库进行多线程编程,我相信本章也一定可以让你收获良多。

md/04同步操作.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ int main(){
332332

333333
> [运行](https://godbolt.org/z/fEvs3M3vv)测试。
334334
335-
如你所见,它支持所有[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,并且也是默认复制,必须使用 `std::ref` 才能传递引用。并且它和 `std::thread` 一样,内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以如果不使用 `std::ref`,这里 `void f(int&)` 就会导致编译错误,如果是 `void f(const int&)` 则可以通过编译,不过引用的不是我们传递的局部对象。
335+
如你所见,它支持所有[可调用(Callable)](https://zh.cppreference.com/w/cpp/named_req/Callable)对象,并且也是默认按值复制,必须使用 `std::ref` 才能传递引用。并且它和 `std::thread` 一样,内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以如果不使用 `std::ref`,这里 `void f(int&)` 就会导致编译错误,如果是 `void f(const int&)` 则可以通过编译,不过引用的不是我们传递的局部对象。
336336

337337
```cpp
338338
void f(const int& p) {}

md/05内存模型与原子操作.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ x = b.exchange(false, std::memory_order_acq_rel);
387387
388388
假设有两个线程运行 `try_set_flag` 函数,那么第一个线程调用 `compare_exchange_strong` 将原子对象 `flag` 设置为 `true`。第二个线程调用 `compare_exchange_strong`,当前原子对象的值为 `true`,而 `expected``false`,不相等,将原子对象的值设置给 `expected`。此时 `flag``expected` 均为 `true`
389389

390-
`exchang` 的另一个不同是,`compare_exchange_weak``compare_exchange_strong` 允许指定成功和失败情况下的内存序。这意味着你可以根据成功或失败的情况,为原子操作指定不同的内存序。
390+
`exchange` 的另一个不同是,`compare_exchange_weak``compare_exchange_strong` 允许指定成功和失败情况下的内存序。这意味着你可以根据成功或失败的情况,为原子操作指定不同的内存序。
391391

392392
```cpp
393393
std::atomic<bool> data{ false };

0 commit comments

Comments
 (0)