Skip to content

Commit 84c165f

Browse files
committed
fut and pro
1 parent b95e9a1 commit 84c165f

File tree

4 files changed

+81
-1
lines changed

4 files changed

+81
-1
lines changed

docs/parallel/multi-threading.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ terminate called without an active exception
271271

272272
而使用互斥量的时候,则是直接对数据结构加锁,同时只有一个线程在修改数据,修改完了再解锁,允许下一个线程进入临界区,等于将线程排队。这是一种悲观的(Pessimistic)方法,因为它认为发生冲突的概率大于不冲突的概率。
273273

274-
原子量和互斥量各有优劣,原子量缩小了临界区,增大了并发度,但是在冲突较多的时候需要大量地回退和重试,而且不好写。互斥量的临界区较大,并发度低,但是理解起来比较容易,也不需要回退和重试。
274+
原子量和互斥量各有优劣,原子量缩小了临界区,增大了并发度,但是在冲突较多的时候需要大量地回退和重试,而且不好写。互斥量的临界区较大,并发度低,在使用不当的情况下可能出现死锁问题,但是理解起来比较容易,也不需要回退和重试。
275275

276276
那么应该什么时候使用原子量,什么时候使用互斥量?从开发者的角度来讲,在项目开发的初期,推荐使用互斥量,因为它编写和理解起来都比较简单。而在项目稳定后,经过性能分析,确定了互斥量是性能瓶颈之后,再想办法修改为使用原子量的并发控制。而且,使用原子量编写一个数据结构很容易,但是编写一个**正确的**数据结构是一个非常困难的问题,需要严苛的测试甚至是严格的数学证明。在工程实践中,建议也是直接使用经过大量测试以及验证的成熟的数据结构库。
277277

@@ -335,3 +335,6 @@ terminate called without an active exception
335335
由于各种原因,通过 `wait()` 而被阻塞的线程有可能会在条件变量并没有被调用 `notify()` 等函数时被唤醒,这种现象被称为 spurious wakeup。此时等待线程会拿锁,然后检查输入的函数的返回值,此时返回值一般为 `false`(因为此时确实没有新数据),然后再一次进入阻塞状态。在这个过程中没有发生数据竞争。
336336

337337
条件变量的设计天然地防止了问题的产生。我们还可以尝试将 `producer()` 中的 `cv.notify_one()` 改为 `cv.notify_all()`,它会唤醒所有等待线程,但与此同时我们只输出了一个新数据。结果是:等待线程只会有一个拿到这个新数据,其他线程会再一次进入阻塞状态,仍然没有数据竞争的出现。
338+
339+
???+note "轮询与等待队列"
340+
在繁忙的程序中,队列可能几乎每时每刻都有数据产生,此时直接使用循环轮询,避免使用互斥量以及条件变量,可能会取得更高的性能,更加充分利用 CPU。

docs/parallel/practice/callback.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# 回调模型
2+
3+
有时候我们希望开一个线程去执行一些耗时的操作,例如执行一个很复杂的计算,或者发送一个网络请求,并且想得到函数执行的结果。一种办法是使用一个回调函数接受结果。例如:
4+
5+
```cpp
6+
#include <iostream>
7+
#include <chrono>
8+
#include <thread>
9+
#include <functional>
10+
11+
int calculate(int arg) {
12+
std::this_thread::sleep_for(std::chrono::seconds(1));
13+
return arg + 42;
14+
}
15+
16+
void do_work(int arg, std::function<void(int)> callback) {
17+
int result = calculate(arg);
18+
callback(result);
19+
}
20+
21+
int main() {
22+
std::thread t(do_work, 1, [](int result) {
23+
std::cout << result << std::endl;
24+
});
25+
std::cout << "Continue main" << std::endl;
26+
t.join();
27+
return 0;
28+
}
29+
```
30+
31+
对于普通的函数,我们只需要用一个包装函数来负责将结果传递给回调函数,然后开启一个新的线程去调用这个包装函数即可。不过,回调函数容易陷入回调地狱中,当执行的操作一多起来的时候,就会产生非常深的回调嵌套。

docs/parallel/practice/promise.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Promise 模式
2+
3+
Promise 是“承诺”的意思,看上去并不是很好理解,它的作用是将一个值存放起来,并提供给另一个线程读取。举个例子:
4+
5+
```cpp
6+
#include <iostream>
7+
#include <chrono>
8+
#include <thread>
9+
#include <functional>
10+
#include <future>
11+
12+
int calculate(int arg) {
13+
std::this_thread::sleep_for(std::chrono::seconds(1));
14+
return arg + 42;
15+
}
16+
17+
void do_work(int arg, std::promise<int> p) {
18+
int result = calculate(arg);
19+
p.set_value(result);
20+
}
21+
22+
int main() {
23+
std::promise<int> p;
24+
std::future<int> fut = p.get_future();
25+
std::thread t(do_work, 1, std::move(p));
26+
t.detach();
27+
std::cout << "Continue main" << std::endl;
28+
std::cout << fut.get() << std::endl;
29+
return 0;
30+
}
31+
```
32+
33+
可以看到,promise 的使用方法也不太直观,有点像[回调模式](callback.md),但又不完全像,还需要通过一个 `future` 才能拿到返回值。`future` 又是什么?为什么不能直接从 `promise` 取值?通过文档可以看到,`future` 只有 `get_value()`,而 `promise` 只有 `set_value()` 操作。也就是说,给 `promise` 设置值之后,甚至不能直接从里面将值获取回来,而 `future` 只能通过其他类来设置值,作为使用者只能从中获取值。
34+
35+
这种设计的逻辑是,通过类型来区分值的提供者和使用者,提供者不应该使用值而使用者不应该设置值,并通过 API 保证共享状态不会被错误地修改。
36+
37+
而在没有设置值的时候,调用 `future` 的 `get()` 会阻塞并挂起线程,一旦有值则唤醒线程并返回值。可以理解为:
38+
39+
```cpp
40+
cv.wait(lk, []() { state == READY });
41+
return value;
42+
```
43+
44+
使用 Promise 可以避免回调地狱,用更自然地方法编写程序。

mkdocs.yml

+2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ nav:
109109
- 实战:
110110
- 多线程共享队列: "parallel/practice/multi-threading-queue.md"
111111
- 多进程共享队列: "parallel/practice/multi-processing-queue.md"
112+
- 回调模型: "parallel/practice/callback.md"
113+
- "Promise 模型": "parallel/practice/promise.md"
112114
- 线程池: "parallel/practice/thread-pool.md"
113115
- 进程池: "parallel/practice/process-pool.md"
114116
- 网络编程 RPC 入门:

0 commit comments

Comments
 (0)