Skip to content

Commit f65a1c0

Browse files
committed
1. 修改第三章的“使用互斥量”中的用词与格式,增加脚注
2. 为第三章增加三级标题 `std::lock_guard`,且增加一小节 `try_lock` 3. 修改第三章“死锁”的一个字 4. 修改第三章“`new`、`delete` 是线程安全的吗?”这一节的格式 5. 为第四章增加一节内容 “等待事件或条件” 完成大部分
1 parent 5733a17 commit f65a1c0

File tree

2 files changed

+140
-6
lines changed

2 files changed

+140
-6
lines changed

md/03共享数据.md

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,13 @@ std::thread t1{f}, t2{f}, t3{f}; // 未定义行为
7373
7474
## 使用互斥量
7575
76-
互斥量(Mutex),又称为互斥锁, 是一种用来保护**临界区**的特殊对象,它可以处于锁定(locked) 状态, 也可以处于解锁(unlocked) 状态:
76+
互斥量(Mutex),又称为互斥锁,是一种用来保护**临界区**[^1]的特殊对象,它可以处于锁定(locked)状态,也可以处于解锁(unlocked)状态:
7777
78-
1如果互斥锁是锁定的, 通常说某个特定的线程正持有这个互斥锁
78+
1. 如果互斥锁是锁定的, 通常说某个特定的线程正持有这个互斥锁
7979
80-
2、如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态
80+
2. 如果没有线程持有这个互斥量,那么这个互斥量就处于解锁状态
81+
82+
[^1]: "***[临界区](https://zh.wikipedia.org/wiki/%E8%87%A8%E7%95%8C%E5%8D%80%E6%AE%B5)***"指的是一个访问共享资源的程序片段,而这些共享资源又无法同时被多个线程访问的特性。在临界区中,通常会使用同步机制,比如我们要讲的互斥量(Mutex)。
8183
8284
---
8385
@@ -128,6 +130,8 @@ int main() {
128130
129131
看一遍描述就可以了,简而言之,被 `lock()``unlock()` 包含在其中的代码是线程安全的,同一时间只有一个线程执行,不会被其它线程的执行所打断。
130132

133+
### `std::lock_guard`
134+
131135
不过一般不推荐这样显式的 `lock()``unlock()`,我们可以使用 C++11 标准库引入的“管理类” [`std::lock_guard`](https://zh.cppreference.com/w/cpp/thread/lock_guard)
132136

133137
```cpp
@@ -257,6 +261,43 @@ std::scoped_lock lc{ m }; // std::scoped_lock<std::mutex>
257261

258262
我们在后续管理多个互斥量,会详细了解这个类。
259263

264+
### `try_lock`
265+
266+
`try_lock` 是互斥量中的一种尝试上锁的方式。与常规的 `lock` 不同,`try_lock` 会尝试上锁,但如果锁已经被其他线程占用,则**不会阻塞当前线程,而是立即返回**
267+
268+
它的返回类型是 `bool` ,如果上锁成功就返回 `true`,失败就返回 `false`
269+
270+
这种方法在多线程编程中很有用,特别是在需要保护临界区的同时,又不想线程因为等待锁而阻塞的情况下。
271+
272+
```cpp
273+
std::mutex mtx;
274+
275+
void threadFunction(int id) {
276+
// 尝试加锁
277+
if (mtx.try_lock()) {
278+
std::cout << "线程:" << id << " 获得锁" << std::endl;
279+
// 临界区代码
280+
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟临界区操作
281+
mtx.unlock(); // 解锁
282+
std::cout << "线程:" << id << " 释放锁" << std::endl;
283+
} else {
284+
std::cout << "线程:" << id << " 获取锁失败 处理步骤" << std::endl;
285+
}
286+
}
287+
```
288+
289+
如果有两个线程运行这段代码,必然有一个线程无法成功上锁,要走 else 的分支。
290+
291+
```cpp
292+
std::thread t1(threadFunction, 1);
293+
std::thread t2(threadFunction, 2);
294+
295+
t1.join();
296+
t2.join();
297+
```
298+
299+
> [运行](https://godbolt.org/z/ajjxnPGMG)测试。
300+
260301
## 保护共享数据
261302

262303
互斥量主要也就是为了保护共享数据,上一节的*使用互斥量*也已经为各位展示了一些。
@@ -310,7 +351,7 @@ void foo(){
310351
311352
试想一下,有一个玩具,这个玩具有两个部分,必须同时拿到两部分才能玩。比如一个遥控汽车,需要遥控器和玩具车才能玩。有两个小孩,他们都想玩这个玩具。当其中一个小孩拿到了遥控器和玩具车时,就可以尽情玩耍。当另一个小孩也想玩,他就得等待另一个小孩玩完才行。再试想,遥控器和玩具车被放在两个不同的地方,并且两个小孩都想要玩,并且一个拿到了遥控器,另一个拿到了玩具车。问题就出现了,除非其中一个孩子决定让另一个先玩,他把自己的那个部分给另一个小孩。但如果他们都不愿意,那么这个遥控汽车就谁都没有办法玩。
312353
313-
我们当然不在于小孩抢玩具,我们要聊的是线程对锁的竞争:*两个线程需要对它们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个线程的互斥量解锁。因为它们都在等待对方释放互斥量,没有线程工作。* 这种情况就是死锁。
354+
我们当然不在乎小孩抢玩具,我们要聊的是线程对锁的竞争:*两个线程需要对它们所有的互斥量做一些操作,其中每个线程都有一个互斥量,且等待另一个线程的互斥量解锁。因为它们都在等待对方释放互斥量,没有线程工作。* 这种情况就是死锁。
314355
315356
- **多个互斥量才可能遇到死锁问题**。
316357
@@ -781,8 +822,8 @@ void recursiveFunction(int count) {
781822
782823
如果你的标准达到 **C++11**,要求下列**函数**是线程安全的:
783824
784-
- [**`new` 运算符**](https://zh.cppreference.com/w/cpp/memory/new/operator_new)[`delete` 运算符](https://zh.cppreference.com/w/cpp/memory/new/operator_delete)****版本
785-
- 全局 **`new` 运算符** `delete` 运算符的用户替换版本
825+
- [`new` 运算符](https://zh.cppreference.com/w/cpp/memory/new/operator_new)[`delete` 运算符](https://zh.cppreference.com/w/cpp/memory/new/operator_delete)****版本
826+
- 全局 `new` 运算符和 `delete` 运算符的用户替换版本
786827
- [std::calloc](https://zh.cppreference.com/w/cpp/memory/c/calloc)[std::malloc](https://zh.cppreference.com/w/cpp/memory/c/malloc)[std::realloc](https://zh.cppreference.com/w/cpp/memory/c/realloc)[std::aligned_alloc](https://zh.cppreference.com/w/cpp/memory/c/aligned_alloc) (C++17 起)、[std::free](https://zh.cppreference.com/w/cpp/memory/c/free)
787828
788829
所以以下函数在多线程运行是线程安全的:

md/04同步操作.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,96 @@
99
- `std::future` 等待异步任务
1010

1111
- 在规定时间内等待
12+
13+
本章将讨论如何使用条件变量等待事件,介绍 future,等标准库设施用作同步操作。
14+
15+
## 等待事件或条件
16+
17+
假设你正在一辆夜间运行的地铁上,那么你在正确的站点下车呢?
18+
19+
1. 一直不休息,每一站都能知道,这样就不会错过你要下车的站点,但是这会很疲惫。
20+
21+
2. 可以看一下时间,估算一下火车到达目的地的时间,然后设置一个稍早的闹钟,就休息。这个方法听起来还行,但是你可能被过早的叫醒,甚至估算错误,以及坐过站,又或者闹钟没电了,睡过站。
22+
23+
3. 事实上最简单的方式是,到站的时候有人或者其它东西能将你叫醒(比如手机的地图,到达设置的位置就提醒)。
24+
25+
这和线程有什么关系呢?其实第一种方法就是在说轮询。
26+
27+
```cpp
28+
bool flag = false;
29+
std::mutex m;
30+
31+
void wait_for_flag(){
32+
std::unique_lock<std::mutex>lk{ m };
33+
while (!flag){
34+
lk.unlock(); // 1 解锁互斥量
35+
lk.lock(); // 2 上锁互斥量
36+
}
37+
}
38+
```
39+
40+
第二种方法就是加个延时,这种实现进步了很多,减少浪费的执行时间,但很难确定正确的休眠时间。这会影响到程序的行为,在需要快速响应的程序中就意味着丢帧或错过了一个时间片。
41+
42+
```cpp
43+
void wait_for_flag(){
44+
std::unique_lock<std::mutex>lk{ m };
45+
while (!flag){
46+
lk.unlock(); // 1 解锁互斥量
47+
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠
48+
lk.lock(); // 3 上锁互斥量
49+
}
50+
}
51+
```
52+
53+
第三种方式(也是最好的)实际上就是使用条件变量了。通过另一线程触发等待事件的机制是最基本的唤醒方式,这种机制就称为“条件变量”。
54+
55+
C++ 标准库对条件变量有两套实现:[`std::condition_variable`](https://zh.cppreference.com/w/cpp/thread/condition_variable)[`std::condition_variable_any`](https://zh.cppreference.com/w/cpp/thread/condition_variable_any),这两个实现都包含在 [`<condition_variable>`](https://zh.cppreference.com/w/cpp/header/condition_variable) 头文件中。
56+
57+
`condition_variable_any` 类是 `std::condition_variable` 的泛化。相对于只在 `std::unique_lock<std::mutex>` 上工作的 `std::condition_variable``condition_variable_any` 能在任何满足[*可基本锁定(BasicLockable)*](https://zh.cppreference.com/w/cpp/named_req/BasicLockable)要求的锁上工作,所以增加了 `_any` 后缀。显而易见,这种区分必然是 `any`**更加通用但是却又更多的性能开销**。所以通常**首选** `std::condition_variable`。有特殊需求,才会考虑 `std::condition_variable_any`
58+
59+
```cpp
60+
std::mutex mtx;
61+
std::condition_variable cv;
62+
bool arrived = false;
63+
64+
void waitForArrival() {
65+
std::unique_lock<std::mutex> lck(mtx);
66+
cv.wait(lck, []{ return arrived; }); // 等待 arrived 变为 true
67+
std::cout << "到达目的地,可以下车了!" << std::endl;
68+
}
69+
70+
void simulateArrival() {
71+
std::this_thread::sleep_for(std::chrono::seconds(5)); // 模拟地铁到站,假设5秒后到达目的地
72+
{
73+
std::lock_guard<std::mutex> lck(mtx);
74+
arrived = true; // 设置条件变量为 true,表示到达目的地
75+
}
76+
cv.notify_one(); // 通知等待的线程
77+
}
78+
```
79+
80+
> [运行](https://godbolt.org/z/eEaMhEanx)测试。更换为 `std::condition_variable_any` 效果[相同](https://godbolt.org/z/8dcPsKd5q)
81+
82+
- `std::mutex mtx`: 创建了一个互斥量,用于保护共享数据的访问,确保在多线程环境下的数据同步。
83+
84+
- `std::condition_variable cv`: 创建了一个条件变量,用于线程间的同步,当条件不满足时,线程可以等待,直到条件满足时被唤醒。
85+
86+
- `bool arrived = false`: 设置了一个标志位,表示是否到达目的地。
87+
88+
`waitForArrival` 函数中:
89+
90+
1. `std::unique_lock<std::mutex> lck(mtx)`: 使用互斥量创建了一个独占锁。
91+
92+
2. `cv.wait(lck, []{ return arrived; })`: 在等待期间,当前线程会**释放锁**(unlock)并**等待**条件变量 `arrived` 变为 true。`cv.wait` 方法会自动释放锁并将当前线程加入到条件变量的等待队列中,直到被唤醒。
93+
94+
3. 一旦条件满足,即 `arrived` 变为 true,并且当前线程被其它线程唤醒,那么当前线程会重新获取锁(lock),并执行后续的操作。
95+
96+
`simulateArrival` 函数中:
97+
98+
1. `std::this_thread::sleep_for(std::chrono::seconds(5))`: 模拟地铁到站,暂停当前线程 5 秒。
99+
100+
2. 设置 `arrived` 为 true,表示到达目的地。
101+
102+
3. `cv.notify_one()`: 通知等待在条件变量上的一个线程,唤醒其中一个等待线程。
103+
104+
这样,当 `simulateArrival` 函数执行后,`arrived` 被设置为 true,并且通过 `cv.notify_one()` 唤醒了等待在条件变量上的线程,从而使得 `waitForArrival` 函数中的等待结束,可以执行后续的操作,即输出提示信息。

0 commit comments

Comments
 (0)