Skip to content

Commit 733a43b

Browse files
committed
1. 修改第四章中线程安全队列的代码格式
2. 修改第五章,完善 `std::atomic<std::shared_ptr>` 的内容,先补充说明 `std::shared_ptr` 本身的问题 3. 在最开头强调 atomic 初始化不是原子操作
1 parent 639e052 commit 733a43b

File tree

2 files changed

+94
-9
lines changed

2 files changed

+94
-9
lines changed

md/04同步操作.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,28 +149,28 @@ public:
149149
threadsafe_queue() {}
150150
void push(T new_value) {
151151
{
152-
std::lock_guard<std::mutex>lk(m);
152+
std::lock_guard<std::mutex>lk { m };
153153
data_queue.push(new_value);
154154
}
155155
data_cond.notify_one();
156156
}
157157
// 从队列中弹出元素(阻塞直到队列不为空)
158158
void pop(T& value) {
159-
std::unique_lock<std::mutex>lk(m);
159+
std::unique_lock<std::mutex> lk{ m };
160160
data_cond.wait(lk, [this] {return !data_queue.empty(); });
161161
value = data_queue.front();
162162
data_queue.pop();
163163
}
164164
// 从队列中弹出元素(阻塞直到队列不为空),并返回一个指向弹出元素的 shared_ptr
165165
std::shared_ptr<T> pop() {
166-
std::unique_lock<std::mutex>lk(m);
166+
std::unique_lock<std::mutex> lk{ m };
167167
data_cond.wait(lk, [this] {return !data_queue.empty(); });
168-
std::shared_ptr<T>res(std::make_shared<T>(data_queue.front()));
168+
std::shared_ptr<T>res { std::make_shared<T>(data_queue.front()) };
169169
data_queue.pop();
170170
return res;
171171
}
172172
bool empty()const {
173-
std::lock_guard<std::mutex>lk(m);
173+
std::lock_guard<std::mutex>lk (m);
174174
return data_queue.empty();
175175
}
176176
};
@@ -201,7 +201,7 @@ std::cout << "push:" << new_value << std::endl;
201201
std::cout << "pop:" << value << std::endl;
202202
```
203203

204-
**可能**[**运行**](https://godbolt.org/z/33T44arb8)结果是:
204+
**可能**[**运行**](https://godbolt.org/z/77666WGP8)结果是:
205205

206206
```txt
207207
push:0

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

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ struct trivial_type {
221221
3. **Read-modify-write(读-改-写)操作**:可选的内存序包括 `memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`、`memory_order_seq_cst`。
222222
223223
> 本节主要广泛介绍 `std::atomic`,而未展开具体使用。在后续章节中,我们将更详细地讨论一些版本,如 `std::atomic<bool>`,并介绍其成员函数和使用方法。
224+
>
225+
> 最后强调一下:任何 [std::atomic](https://zh.cppreference.com/w/cpp/atomic/atomic) 类型,**初始化不是原子操作**。
224226
225227
### `st::atomic_flag`
226228
@@ -419,13 +421,96 @@ else {
419421

420422
在前文中,我们多次提到 `std::shared_ptr`
421423

422-
> 第四章中提到:*多个线程能在不同的 shared_ptr 对象上调用**所有成员函数**(包含复制构造函数与复制赋值)而不附加同步,即使这些实例是同一对象的副本且共享所有权也是如此。若多个执行线程访问同一 shared_ptr 对象而不同步,且任一线程使用 shared_ptr 的非 const 成员函数,则将出现数据竞争;`std::atomic<shared_ptr>` 能用于避免数据竞争。[文档](https://zh.cppreference.com/w/cpp/memory/shared_ptr#:~:text=%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E8%83%BD%E5%9C%A8%E4%B8%8D%E5%90%8C%E7%9A%84%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E4%B8%8A%E8%B0%83%E7%94%A8%E6%89%80%E6%9C%89%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%88%E5%8C%85%E5%90%AB%E5%A4%8D%E5%88%B6%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E4%B8%8E%E5%A4%8D%E5%88%B6%E8%B5%8B%E5%80%BC%EF%BC%89%E8%80%8C%E4%B8%8D%E9%99%84%E5%8A%A0%E5%90%8C%E6%AD%A5%EF%BC%8C%E5%8D%B3%E4%BD%BF%E8%BF%99%E4%BA%9B%E5%AE%9E%E4%BE%8B%E6%98%AF%E5%90%8C%E4%B8%80%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%89%AF%E6%9C%AC%E4%B8%94%E5%85%B1%E4%BA%AB%E6%89%80%E6%9C%89%E6%9D%83%E4%B9%9F%E6%98%AF%E5%A6%82%E6%AD%A4%E3%80%82%E8%8B%A5%E5%A4%9A%E4%B8%AA%E6%89%A7%E8%A1%8C%E7%BA%BF%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%90%8C%E4%B8%80%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E8%80%8C%E4%B8%8D%E5%90%8C%E6%AD%A5%EF%BC%8C%E4%B8%94%E4%BB%BB%E4%B8%80%E7%BA%BF%E7%A8%8B%E4%BD%BF%E7%94%A8%20shared_ptr%20%E7%9A%84%E9%9D%9E%20const%20%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%8C%E5%88%99%E5%B0%86%E5%87%BA%E7%8E%B0%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%EF%BC%9Bstd%3A%3Aatomic%3Cshared_ptr%3E%20%E8%83%BD%E7%94%A8%E4%BA%8E%E9%81%BF%E5%85%8D%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%E3%80%82)。*
424+
> 第四章中提到:*多个线程能在不同的 shared_ptr 对象上调用**所有成员函数**(包含复制构造函数与复制赋值)而不附加同步,即使这些实例是同一对象的副本且共享所有权也是如此。若多个执行线程访问**同一 shared_ptr** 对象而不同步,且任一线程使用 shared_ptr 的非 const 成员函数,则将出现数据竞争;`std::atomic<shared_ptr>` 能用于避免数据竞争。[文档](https://zh.cppreference.com/w/cpp/memory/shared_ptr#:~:text=%E5%A4%9A%E4%B8%AA%E7%BA%BF%E7%A8%8B%E8%83%BD%E5%9C%A8%E4%B8%8D%E5%90%8C%E7%9A%84%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E4%B8%8A%E8%B0%83%E7%94%A8%E6%89%80%E6%9C%89%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%88%E5%8C%85%E5%90%AB%E5%A4%8D%E5%88%B6%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E4%B8%8E%E5%A4%8D%E5%88%B6%E8%B5%8B%E5%80%BC%EF%BC%89%E8%80%8C%E4%B8%8D%E9%99%84%E5%8A%A0%E5%90%8C%E6%AD%A5%EF%BC%8C%E5%8D%B3%E4%BD%BF%E8%BF%99%E4%BA%9B%E5%AE%9E%E4%BE%8B%E6%98%AF%E5%90%8C%E4%B8%80%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%89%AF%E6%9C%AC%E4%B8%94%E5%85%B1%E4%BA%AB%E6%89%80%E6%9C%89%E6%9D%83%E4%B9%9F%E6%98%AF%E5%A6%82%E6%AD%A4%E3%80%82%E8%8B%A5%E5%A4%9A%E4%B8%AA%E6%89%A7%E8%A1%8C%E7%BA%BF%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%90%8C%E4%B8%80%20shared_ptr%20%E5%AF%B9%E8%B1%A1%E8%80%8C%E4%B8%8D%E5%90%8C%E6%AD%A5%EF%BC%8C%E4%B8%94%E4%BB%BB%E4%B8%80%E7%BA%BF%E7%A8%8B%E4%BD%BF%E7%94%A8%20shared_ptr%20%E7%9A%84%E9%9D%9E%20const%20%E6%88%90%E5%91%98%E5%87%BD%E6%95%B0%EF%BC%8C%E5%88%99%E5%B0%86%E5%87%BA%E7%8E%B0%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%EF%BC%9Bstd%3A%3Aatomic%3Cshared_ptr%3E%20%E8%83%BD%E7%94%A8%E4%BA%8E%E9%81%BF%E5%85%8D%E6%95%B0%E6%8D%AE%E7%AB%9E%E4%BA%89%E3%80%82)。*
423425

424426
一个在互联网上非常热门的八股问题是:***`std::shared_ptr` 是不是线程安全?***
425427

426-
显然,它并不是完全线程安全的,尽管在多线程环境中有一定保证,但这还不够。在 C++20 中,原子模板 `std::atomic` 引入了一个偏特化版本 [`std::atomic<std::shared_ptr>`](https://zh.cppreference.com/w/cpp/memory/shared_ptr/atomic2) 允许用户原子地操纵 `shared_ptr` 对象。因为它是 `std::atomic` 的特化版本,即使我们还没有深入讲述它,也能知道它是**原子类型**,这意味着它的所有操作都是**原子操作**
428+
显然,它并不是完全线程安全的,尽管在多线程环境中有很大的保证,但这还不够。在 C++20 中,原子模板 `std::atomic` 引入了一个偏特化版本 [`std::atomic<std::shared_ptr>`](https://zh.cppreference.com/w/cpp/memory/shared_ptr/atomic2) 允许用户原子地操纵 `shared_ptr` 对象。因为它是 `std::atomic` 的特化版本,即使我们还没有深入讲述它,也能知道它是**原子类型**,这意味着它的所有操作都是**原子操作**
429+
430+
若多个执行线程不同步地同时访问**同一** `std::shared_ptr` 对象,且任何这些访问使用了 shared_ptr 的**非 const 成员函数**,则将**出现数据竞争****除非通过 `std::atomic<std::shared_ptr>` 的实例进行所有访问**
431+
432+
```cpp
433+
class Data {
434+
public:
435+
Data(int value = 0) : value_(value) {}
436+
int get_value() const { return value_; }
437+
void set_value(int new_value) { value_ = new_value; }
438+
private:
439+
int value_;
440+
};
441+
442+
auto data = std::make_shared<Data>();
443+
444+
void writer(){
445+
for (int i = 0; i < 10; ++i) {
446+
std::shared_ptr<Data> new_data = std::make_shared<Data>(i);
447+
data.swap(new_data); // 调用非 const 成员函数
448+
std::this_thread::sleep_for(100ms);
449+
}
450+
}
451+
452+
void reader(){
453+
for (int i = 0; i < 10; ++i) {
454+
if (data) {
455+
std::cout << "读取线程值: " << data->get_value() << std::endl;
456+
}
457+
else {
458+
std::cout << "没有读取到数据" << std::endl;
459+
}
460+
std::this_thread::sleep_for(100ms);
461+
}
462+
}
463+
464+
int main(){
465+
std::thread writer_thread{ writer };
466+
std::thread reader_thread{ reader };
467+
468+
writer_thread.join();
469+
reader_thread.join();
470+
}
471+
```
472+
473+
> [运行](https://godbolt.org/z/6zo7hK8h1)测试。
474+
475+
以上这段代码是典型的**线程不安全**,它满足:
476+
477+
- [x] 多个线程不同步地同时访问**同一** `std::shared_ptr` 对象
478+
479+
- [x] 任一线程使用 shared_ptr 的**非 const** 成员函数
480+
481+
---
482+
483+
不过事实上 `std::atomic<std::shared_ptr>` 的功能相当有限,单看它提供的修改接口(`=`、`store`、`load`、`exchang`)就能明白。如果要操作其保护的共享指针指向的资源还是得 `load()` 获取底层共享指针的副本。
484+
485+
486+
487+
---
488+
489+
在使用 `std::atomic<std::shared_ptr>` 的时候,我们要注意第三章中关于共享数据的一句话:
490+
491+
> **切勿将受保护数据的指针或引用传递到互斥量作用域之外**,不然保护将**形同虚设**。
492+
493+
原子类型也有类似的问题,以下是一个例子:
494+
495+
```cpp
496+
std::atomic<std::shared_ptr<int>> ptr = std::make_shared<int>(10);
497+
*ptr.load() = 100;
498+
```
499+
500+
1. 调用 `load()` 成员函数,原子地返回底层共享指针的**副本** `std::shared_ptr`
501+
2. 解引用,等价 `*get()`,返回了 `int&`
502+
3. 直接修改这个引用所指向的资源。
503+
504+
在第一步时,已经脱离了 `std::atomic` 的保护,第二步就获取了被保护的数据的引用,第三步进行了修改,这导致了数据竞争。当然了,这是做法非常的愚蠢,只是为了表示,所谓的线程安全,也是要靠**开发者的正确使用**
505+
506+
正确的用法如下:
507+
508+
```cpp
509+
std::atomic<std::shared_ptr<int>> ptr = std::make_shared<int>(10);
510+
ptr.store(std::make_shared<int>(100));
511+
```
427512

428-
若多个执行线程不同步地同时访问同一 `std::shared_ptr` 对象,且任何这些访问使用了 shared_ptr 的非 const 成员函数,则将**出现数据竞争****除非通过 `std::atomic<std::shared_ptr>` 的实例进行所有访问**
513+
通过使用 `store` 成员函数,可以原子地替换 `ptr` 所保护的值
429514

430515
## 内存次序
431516

0 commit comments

Comments
 (0)