Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /