Skip to content

Commit f9d3eb5

Browse files
committed
1. 考虑“复制”与“拷贝”这种用词同时存在,可能存在混乱和不统一,故将所有使用“拷贝”这种用词的均改为“复制”,统一
2. 修改第二章部分代码注释以及各式 3. 增修改第四章,增加代码注释,修改部分解释 4. 第五章调整标题等级,下降为三级;完成部分 `std::atomic<bool>` 的内容,创建 `std::atomic<T*>` 与 `std::atomic<std::shared_ptr>` 这两个三级标题
1 parent b39496c commit f9d3eb5

File tree

4 files changed

+64
-18
lines changed

4 files changed

+64
-18
lines changed

md/02使用线程.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ class thread_guard{
361361
public:
362362
explicit thread_guard(std::thread& t) :m_t{ t } {}
363363
~thread_guard(){
364-
std::puts("析构"); // 打印 不用在乎
365-
if (m_t.joinable()) { // 没有关联活跃线程
364+
std::puts("析构"); // 打印日志 不用在乎
365+
if (m_t.joinable()) { // 线程对象当前关联了活跃线程
366366
m_t.join();
367367
}
368368
}
@@ -377,23 +377,23 @@ void f(){
377377
}
378378
```
379379
380-
函数 f 执行完毕,局部对象就要逆序销毁了。因此,thread_guard 对象 g 是第一个被销毁的,**调用析构函数****即使函数 f2() 抛出了一个异常,这个销毁依然会发生(前提是你捕获了这个异常)**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/MaWjW73P4)
380+
函数 f 执行完毕,局部对象就要逆序销毁了。因此,thread_guard 对象 g 是第一个被销毁的,**调用析构函数****即使函数 f2() 抛出了一个异常,这个销毁依然会发生(前提是你捕获了这个异常)**。这确保了线程对象 t 所关联的线程正常的执行完毕以及线程对象的正常析构。[测试代码](https://godbolt.org/z/hn7Gced84)
381381
382382
> 如果异常被抛出但未被捕获那么就会调用 [std::terminate](https://zh.cppreference.com/w/cpp/error/terminate)。是否对未捕获的异常进行任何栈回溯由**实现定义**。(简单的说就是不一定会调用析构)
383383
>
384-
> 我们的测试代码是捕获了异常的,为了观测,看到它一定打印“析构”。
384+
> 我们的测试代码是捕获了异常的,为了观测,看到它一定打印“*析构*”。
385385
386386
在 thread_guard 的析构函数中,我们要判断 `std::thread` 线程对象现在是否有关联的活跃线程,如果有,我们才会执行 **`join()`**,阻塞当前线程直到线程对象关联的线程执行完毕。如果不想等待线程结束可以使用 `detach()` ,但是这让 `std::thread` 对象失去了线程资源的所有权,难以掌控,具体如何,看情况分析。
387387
388-
拷贝赋值和拷贝构造定义为 `=delete` 可以防止编译器隐式生成,同时会[**阻止**](https://zh.cppreference.com/w/cpp/language/rule_of_three#.E4.BA.94.E4.B9.8B.E6.B3.95.E5.88.99)移动构造函数和移动赋值运算符的隐式定义。这样的话,对 thread_guard 对象进行拷贝或赋值等操作会引发一个编译错误
388+
复制赋值和复制构造定义为 `=delete` 可以防止编译器隐式生成,同时会[**阻止**](https://zh.cppreference.com/w/cpp/language/rule_of_three#.E4.BA.94.E4.B9.8B.E6.B3.95.E5.88.99)移动构造函数和移动赋值运算符的隐式定义。这样的话,对 thread_guard 对象进行复制或赋值等操作会引发一个编译错误
389389
390390
不允许这些操作主要在于:这是个管理类,而且顾名思义,它就应该只是单纯的管理线程对象仅此而已,只保有一个引用,**单纯的做好 RAII 的事情就行,允许其他操作没有价值。**
391391
392392
> 严格来说其实这里倒也不算 RAII,因为 thread_guard 的构造函数其实并没有申请资源,只是保有了线程对象的引用,在析构的时候进行了 join() 。
393393
394394
### 传递参数
395395
396-
向可调用对象或函数传递参数很简单,我们前面也都写了,只需要将这些参数作为 `std::thread` 的构造参数即可。需要注意的是,这些参数会拷贝到新线程的内存空间中,即使函数中的参数是引用,依然**实际是拷贝**
396+
向可调用对象或函数传递参数很简单,我们前面也都写了,只需要将这些参数作为 `std::thread` 的构造参数即可。需要注意的是,这些参数会复制到新线程的内存空间中,即使函数中的参数是引用,依然**实际是复制**
397397
398398
```cpp
399399
void f(int, const int& a);
@@ -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
@@ -419,7 +419,7 @@ int main() {
419419
420420
[运行代码](https://godbolt.org/z/TzWeW5rxh),打印的地址截然不同。
421421
422-
可以通过编译,但通常这不符合我们的需求,因为我们的函数中的参数是引用,我们自然希望能引用调用方传递的参数,而不是拷贝。如果我们的 f 的形参类型不是 **const 的引用**,则会产生一个[编译错误](https://godbolt.org/z/3nMb4asnG)
422+
可以通过编译,但通常这不符合我们的需求,因为我们的函数中的参数是引用,我们自然希望能引用调用方传递的参数,而不是复制。如果我们的 f 的形参类型不是 **const 的引用**,则会产生一个[编译错误](https://godbolt.org/z/3nMb4asnG)
423423
424424
想要解决这个问题很简单,我们可以使用标准库的设施 [`std::ref`](https://zh.cppreference.com/w/cpp/utility/functional/ref)`std::cref` 函数模板。
425425
@@ -458,7 +458,7 @@ const int& p = r; // r 隐式转换为 n 的 const 的引用 此时 p 引用的
458458
459459
---
460460
461-
以上代码`void f(int, int&)` 如果不使用 `std::ref` 并不会和前面 `void f(int, const int&)` 一样只是多了拷贝,而是会产生[**编译错误**](https://godbolt.org/z/xhrhs6Ke5),这是因为 `std::thread` 内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以产生编译错误。
461+
以上代码`void f(int, int&)` 如果不使用 `std::ref` 并不会和前面 `void f(int, const int&)` 一样只是多了复制,而是会产生[**编译错误**](https://godbolt.org/z/xhrhs6Ke5),这是因为 `std::thread` 内部会将保有的参数副本转换为**右值表达式进行传递**,这是为了那些**只支持移动的类型**,左值引用没办法引用右值表达式,所以产生编译错误。
462462
463463
```cpp
464464
struct move_only {
@@ -511,7 +511,7 @@ struct X{
511511
std::thread t{ std::bind(&X::task_run, &x ,n) };
512512
```
513513
514-
不过需要注意,`std::bind` 也是默认[**拷贝**](https://godbolt.org/z/c5bh8Easd)的,即使我们的成员函数形参类型为引用:
514+
不过需要注意,`std::bind` 也是默认按值[**复制**](https://godbolt.org/z/c5bh8Easd)的,即使我们的成员函数形参类型为引用:
515515
516516
```cpp
517517
struct X {

md/04同步操作.md

Lines changed: 4 additions & 3 deletions
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) {}
@@ -457,6 +457,7 @@ std::packaged_task<double(int, int)> task([](int a, int b){
457457
});
458458
std::future<double>future = task.get_future();
459459
std::thread t{ std::move(task),10,2 }; // 任务在线程中执行
460+
// todo.. 幻想还有许多耗时的代码
460461
t.join();
461462

462463
std::cout << future.get() << '\n'; // 并不堵塞,获取任务返回值罢了
@@ -675,7 +676,7 @@ int main() {
675676

676677
之前的例子中都在用 `std::future` ,不过 `std::future` 也有局限性。很多线程在等待的时候,只有一个线程能获取结果。当多个线程等待相同事件的结果时,就需要使用 `std::shared_future` 来替代 `std::future` 了。`std::future``std::shared_future` 的区别就如同 `std::unique_ptr``std::shared_ptr` 一样。
677678

678-
`std::future` 是只能移动的,其所有权可以在不同的对象中互相传递,但只有一个对象可以获得特定的同步结果。而 `std::shared_future` 是可拷贝的,多个对象可以指代同一个共享状态。
679+
`std::future` 是只能移动的,其所有权可以在不同的对象中互相传递,但只有一个对象可以获得特定的同步结果。而 `std::shared_future` 是可复制的,多个对象可以指代同一个共享状态。
679680

680681
在多个线程中对**同一个 **`std::shared_future` 对象进行操作时(如果没有进行同步保护)存在竞争条件。而从多个线程访问同一共享状态,若每个线程都是通过其自身的 `shared_future` 对象**副本**进行访问,则是安全的。
681682

@@ -740,7 +741,7 @@ int main() {
740741
}
741742
```
742743

743-
这样访问的就都是 `std::shared_future` 的副本了,我们的 lambda 是按拷贝传递的,不存在问题。这一点和 `std::shared_ptr` 类似[^1]
744+
这样访问的就都是 `std::shared_future` 的副本了,我们的 lambda 按复制捕获 std::shared_future 对象,每个线程都有一个 shared_future 的副本,这样不会有任何问题。这一点和 `std::shared_ptr` 类似[^1]
744745

745746
`std::promise` 也同,它的 `get_future()` 成员函数一样可以用来构造 `std::shared_future`,虽然它的返回类型是 `std::future`,不过不影响,这是因为 `std::shared_future` 有一个 `std::future<T>&&` 参数的[构造函数](https://zh.cppreference.com/w/cpp/thread/shared_future/shared_future),转移 `std::future` 的所有权。
746747

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ void f() {
3838

3939
不过这显然不是我们的重点。我们想要的是一种**原子类型**,它的所有操作都直接是**原子**的,不需要额外的同步设施进行保护。C++11 引入了原子类型 [`std::atomic`](https://zh.cppreference.com/w/cpp/atomic/atomic),在下节我们会详细讲解。
4040

41-
## 原子类型 `std::atomic`
41+
### 原子类型 `std::atomic`
4242

4343
标准原子类型定义在头文件 [`<atomic>`](https://zh.cppreference.com/w/cpp/header/atomic) 中。这些类型的操作都是原子的,语言定义中只有这些类型的操作是原子的,虽然也可以用互斥量来模拟原子操作(见上文)。标准的原子的类型实现可能是:*它们几乎都有一个 `is_lock_free()` 成员函数,这个函数可以让用户查询某原子类型的操作是直接用的原子指令(返回 `true`),还是内部用了锁实现(返回 `false`)。*
4444

@@ -131,7 +131,7 @@ using atomic_ullong = atomic<unsigned long long>;
131131

132132
---
133133

134-
通常 `std::atomic` 对象不可进行拷贝和赋值,因为它们的[拷贝构造](https://zh.cppreference.com/w/cpp/atomic/atomic/atomic)[拷贝赋值运算符](https://zh.cppreference.com/w/cpp/atomic/atomic/operator%3D)被定义为[弃置](https://zh.cppreference.com/w/cpp/language/function#.E5.BC.83.E7.BD.AE.E5.87.BD.E6.95.B0)的。不过可以**隐式转换**成对应的内置类型,因为它有[转换函数](https://zh.cppreference.com/w/cpp/atomic/atomic/operator_T)
134+
通常 `std::atomic` 对象不可进行复制、移动、赋值,因为它们的[复制构造](https://zh.cppreference.com/w/cpp/atomic/atomic/atomic)[复制赋值运算符](https://zh.cppreference.com/w/cpp/atomic/atomic/operator%3D)被定义为[弃置](https://zh.cppreference.com/w/cpp/language/function#.E5.BC.83.E7.BD.AE.E5.87.BD.E6.95.B0)的。不过可以**隐式转换**成对应的内置类型,因为它有[转换函数](https://zh.cppreference.com/w/cpp/atomic/atomic/operator_T)
135135

136136
```cpp
137137
atomic(const atomic&) = delete;
@@ -216,7 +216,7 @@ struct trivial_type {
216216
217217
3. **Read-modify-write(读-改-写)操作**:可选的内存序包括 `memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`、`memory_order_seq_cst`。
218218
219-
## `st::atomic_flag`
219+
### `st::atomic_flag`
220220
221221
`std::atomic_flag` 是最简单的原子类型,这个类型的对象可以在两个状态间切换:**设置(true)**和**清除(false)**。它很简单,通常只是用作构建一些库设施,不会单独使用或直接面向普通开发者。
222222
@@ -259,7 +259,7 @@ bool r = f.test_and_set();
259259

260260
> 不用着急,这里还不是详细展开聊内存序的时候。
261261
262-
`std::atomic_flag` [不可复制](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)不可移动[不可赋值](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/operator%3D)。这不是 `std::atomic_flag` 特有的,而是所有原子类型共有的属性。原子类型的所有操作都是原子的,而赋值和拷贝涉及两个对象,破坏了操作的原子性。拷贝构造和拷贝赋值会先读取第一个对象的值,然后再写入另一个对象。对于两个独立的对象,这里实际上有两个独立的操作,合并这两个操作无法保证其原子性。因此,这些操作是不被允许的。
262+
`std::atomic_flag` [不可复制](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/atomic_flag)不可移动[不可赋值](https://zh.cppreference.com/w/cpp/atomic/atomic_flag/operator%3D)。这不是 `std::atomic_flag` 特有的,而是所有原子类型共有的属性。原子类型的所有操作都是原子的,而赋值和复制涉及两个对象,破坏了操作的原子性。复制构造和复制赋值会先读取第一个对象的值,然后再写入另一个对象。对于两个独立的对象,这里实际上有两个独立的操作,合并这两个操作无法保证其原子性。因此,这些操作是不被允许的。
263263

264264
有限的特性使得 `std::atomic_flag` 非常适合用作制作**自旋锁**
265265

@@ -304,6 +304,51 @@ void f(){
304304
305305
`std::atomic_flag` 的局限性太强,甚至不能当普通的 bool 标志那样使用。一般最好使用 `std::atomic<bool>`,下节,我们来使用它。
306306

307+
### `std::atomic<bool>`
308+
309+
`std::atomic<bool>` 是最基本的**整数原子类型** ,它相较于 `std::atomic_flag` 提供了更加完善的布尔标志。虽然同样不可复制不可移动,但可以使用非原子的 bool 类型进行构造,初始化为 true 或 false,并且能从非原子的 bool 对象赋值给 `std::atomic<bool>`
310+
311+
```cpp
312+
std::atomic<bool> b{ true };
313+
b = false;
314+
```
315+
316+
不过这个 [`operator=`](https://zh.cppreference.com/w/cpp/atomic/atomic/operator%3D) 不同于通常情况,赋值操作 `b = false` 返回一个普通的 `bool` 值。
317+
318+
> 这个行为不仅仅适用于`std::atomic<bool>`,而是适用于所有`std::atomic`类型。
319+
320+
如果原子变量的赋值操作返回了一个引用,那么依赖这个结果的代码需要显式地进行加载(load),以确保数据的正确性。例如:
321+
322+
```cpp
323+
std::atomic<bool>b {true};
324+
auto& ref = (b = false); // 假设返回 atomic 引用
325+
bool flag = ref.load(); // 必须显式调用 load() 加载
326+
```
327+
328+
通过返回非原子值进行赋值,可以避免多余的加载(load)过程,得到实际存储的值。
329+
330+
```cpp
331+
std::atomic<bool> b{ true };
332+
bool new_value = (b = false); // new_value 将是 false
333+
```
334+
335+
使用 `store` 原子的替换当前对象的值,远好于 `std::atomic_flag` 的 `clear()`。`test_and_set()` 也可以换为更加通用常见的 `exchange`,它可以原子的使用新的值替换已经存储的值,并返回旧值。
336+
337+
获取 `std::atomic<bool>` 的值有两种方式,调用 `load()` 函数,或者[隐式转换](https://zh.cppreference.com/w/cpp/atomic/atomic/operator_T)。
338+
339+
`store` 是一个存储操作、`load` 是一个加载操作、`exchange` 是一个“读-改-写”操作:
340+
341+
```cpp
342+
std::atomic<bool> b;
343+
bool x = b.load(std::memory_order_acquire);
344+
b.store(true);
345+
x = b.exchange(false, std::memory_order_acq_rel);
346+
```
347+
348+
### `std::atomic<T*>`
349+
350+
### `std::atomic<std::shared_ptr>`
351+
307352
## 内存次序
308353

309354
### 前言

md/详细分析/02scoped_lock源码解析.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## `std::scoped_lock` 的数据成员
1010

11-
`std::scoped_lock` 是一个类模板,它有两个特化,也就是有三个版本,其中的数据成员也是不同的。并且它们都不可移动不可拷贝,“*管理类*”应该如此。
11+
`std::scoped_lock` 是一个类模板,它有两个特化,也就是有三个版本,其中的数据成员也是不同的。并且它们都不可移动不可复制,“*管理类*”应该如此。
1212

1313
1. 主模板,是一个可变参数类模板,声明了一个类型形参包 `_Mutexes`**存储了一个 `std::tuple`**,具体类型根据类型形参包决定。
1414

0 commit comments

Comments
 (0)