File tree 4 files changed +81
-1
lines changed
4 files changed +81
-1
lines changed Original file line number Diff line number Diff line change @@ -271,7 +271,7 @@ terminate called without an active exception
271
271
272
272
而使用互斥量的时候,则是直接对数据结构加锁,同时只有一个线程在修改数据,修改完了再解锁,允许下一个线程进入临界区,等于将线程排队。这是一种悲观的(Pessimistic)方法,因为它认为发生冲突的概率大于不冲突的概率。
273
273
274
- 原子量和互斥量各有优劣,原子量缩小了临界区,增大了并发度,但是在冲突较多的时候需要大量地回退和重试,而且不好写。互斥量的临界区较大,并发度低,但是理解起来比较容易,也不需要回退和重试。
274
+ 原子量和互斥量各有优劣,原子量缩小了临界区,增大了并发度,但是在冲突较多的时候需要大量地回退和重试,而且不好写。互斥量的临界区较大,并发度低,在使用不当的情况下可能出现死锁问题, 但是理解起来比较容易,也不需要回退和重试。
275
275
276
276
那么应该什么时候使用原子量,什么时候使用互斥量?从开发者的角度来讲,在项目开发的初期,推荐使用互斥量,因为它编写和理解起来都比较简单。而在项目稳定后,经过性能分析,确定了互斥量是性能瓶颈之后,再想办法修改为使用原子量的并发控制。而且,使用原子量编写一个数据结构很容易,但是编写一个** 正确的** 数据结构是一个非常困难的问题,需要严苛的测试甚至是严格的数学证明。在工程实践中,建议也是直接使用经过大量测试以及验证的成熟的数据结构库。
277
277
@@ -335,3 +335,6 @@ terminate called without an active exception
335
335
由于各种原因,通过 ` wait() ` 而被阻塞的线程有可能会在条件变量并没有被调用 ` notify() ` 等函数时被唤醒,这种现象被称为 spurious wakeup。此时等待线程会拿锁,然后检查输入的函数的返回值,此时返回值一般为 ` false ` (因为此时确实没有新数据),然后再一次进入阻塞状态。在这个过程中没有发生数据竞争。
336
336
337
337
条件变量的设计天然地防止了问题的产生。我们还可以尝试将 ` producer() ` 中的 ` cv.notify_one() ` 改为 ` cv.notify_all() ` ,它会唤醒所有等待线程,但与此同时我们只输出了一个新数据。结果是:等待线程只会有一个拿到这个新数据,其他线程会再一次进入阻塞状态,仍然没有数据竞争的出现。
338
+
339
+ ???+note "轮询与等待队列"
340
+ 在繁忙的程序中,队列可能几乎每时每刻都有数据产生,此时直接使用循环轮询,避免使用互斥量以及条件变量,可能会取得更高的性能,更加充分利用 CPU。
Original file line number Diff line number Diff line change
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
+ 对于普通的函数,我们只需要用一个包装函数来负责将结果传递给回调函数,然后开启一个新的线程去调用这个包装函数即可。不过,回调函数容易陷入回调地狱中,当执行的操作一多起来的时候,就会产生非常深的回调嵌套。
Original file line number Diff line number Diff line change
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 可以避免回调地狱,用更自然地方法编写程序。
Original file line number Diff line number Diff line change 109
109
- 实战 :
110
110
- 多线程共享队列 : " parallel/practice/multi-threading-queue.md"
111
111
- 多进程共享队列 : " parallel/practice/multi-processing-queue.md"
112
+ - 回调模型 : " parallel/practice/callback.md"
113
+ - " Promise 模型 " : " parallel/practice/promise.md"
112
114
- 线程池 : " parallel/practice/thread-pool.md"
113
115
- 进程池 : " parallel/practice/process-pool.md"
114
116
- 网络编程 RPC 入门 :
You can’t perform that action at this time.
0 commit comments