@@ -6,54 +6,78 @@ order: 7
66
77# 第 7 章 并行与并发
88
9- > 内容修订中
10-
119[ TOC]
1210
1311## 7.1 线程与并行
1412
1513### std::thread
1614
17- ` std::thread ` 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 ` <thread> ` 头文件,它提供了很多基本的线程操作,例如` get_id() ` 来获取所创建线程的线程 ID,例如使用 ` join() ` 来加入一个线程等等,例如:
15+ ` std::thread ` 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 ` <thread> ` 头文件,
16+ 它提供了很多基本的线程操作,例如 ` get_id() ` 来获取所创建线程的线程 ID,
17+ 例如使用 ` join() ` 来加入一个线程等等,例如:
1818
1919``` cpp
2020#include < iostream>
2121#include < thread>
22- void foo () {
23- std::cout << "hello world" << std::endl;
24- }
22+
2523int main () {
26- std::thread t(foo);
24+ std::thread t([](){
25+ std::cout << "hello world." << std::endl;
26+ });
2727 t.join();
2828 return 0;
2929}
3030```
3131
32- ## 7.2 std::mutex, std::unique \_ lock
32+ ## 7.2 互斥量与临界区
3333
34- 我们在操作系统的相关知识中已经了解过了有关并发技术的基本知识,mutex 就是其中的核心之一。C++11引入了 mutex 相关的类,其所有相关的函数都放在 ` <mutex> ` 头文件中。
34+ 我们在操作系统、亦或是数据库的相关知识中已经了解过了有关并发技术的基本知识,` mutex ` 就是其中的核心之一。
35+ C++11 引入了 ` mutex ` 相关的类,其所有相关的函数都放在 ` <mutex> ` 头文件中。
3536
36- ` std::mutex ` 是 C++11 中最基本的 ` mutex ` 类,通过实例化 ` std::mutex ` 可以创建互斥量,而通过其成员函数 ` lock() ` 可以进行上锁,` unlock() ` 可以进行解锁。但是在在实际编写代码的过程中,最好不去直接调用成员函数,因为调用成员函数就需要在每个临界区的出口处调用 ` unlock() ` ,当然,还包括异常。这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类` std::lock_gurad ` 。RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。
37+ ` std::mutex ` 是 C++11 中最基本的 ` mutex ` 类,通过实例化 ` std::mutex ` 可以创建互斥量,
38+ 而通过其成员函数 ` lock() ` 可以进行上锁,` unlock() ` 可以进行解锁。
39+ 但是在在实际编写代码的过程中,最好不去直接调用成员函数,
40+ 因为调用成员函数就需要在每个临界区的出口处调用 ` unlock() ` ,当然,还包括异常。
41+ 这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类 ` std::lock_gurad ` 。
42+ RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。
3743
3844在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分,例如:
3945
4046``` cpp
41- void some_operation (const std::string &message) {
42- static std::mutex mutex;
43- std::lock_guard< std::mutex > lock(mutex);
47+ #include < iostream>
48+ #include < thread>
49+
50+ int v = 1 ;
51+
52+ void critical_section (int change_v) {
53+ static std::mutex mtx;
54+ std::lock_guard< std::mutex > lock(mtx);
55+
56+ // 执行竞争操作
57+ v = change_v;
58+
59+ // 离开此作用域后 mtx 会被释放
60+ }
4461
45- // ...操作
62+ int main() {
63+ std::thread t1(critical_section, 2), t2(critical_section, 3);
64+ t1.join();
65+ t2.join();
4666
47- // 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁
48- // 因此这个函数内部的可以认为是临界区
67+ std::cout << v << std::endl;
68+ return 0;
4969}
5070```
5171
52- 由于 C++保证了所有栈对象在声明周期结束时会被销毁,所以这样的代码也是异常安全的。无论 `some_operation()` 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 `unlock()`。
72+ 由于 C++ 保证了所有栈对象在声明周期结束时会被销毁,所以这样的代码也是异常安全的。
73+ 无论 `critical_section()` 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 `unlock()`。
5374
54- 而 `std::unique_lock` 则相对于 `std::lock_guard` 出现的,`std::unique_lock` 更加灵活,`std::unique_lock` 的对象会以独占所有权(没有其他的 `unique_lock` 对象同时拥有某个 `mutex` 对象的所有权)的方式管理 `mutex` 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 `std::unique_lock`。
75+ 而 `std::unique_lock` 则相对于 `std::lock_guard` 出现的,`std::unique_lock` 更加灵活,
76+ `std::unique_lock` 的对象会以独占所有权(没有其他的 `unique_lock` 对象同时拥有某个 `mutex` 对象的所有权)
77+ 的方式管理 `mutex` 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 `std::unique_lock`。
5578
56- `std::lock_guard` 不能显式的调用 `lock` 和 `unlock`, 而 `std::unique_lock` 可以在声明后的任意位置调用 ,可以缩小锁的作用范围,提供更高的并发度。
79+ `std::lock_guard` 不能显式的调用 `lock` 和 `unlock`, 而 `std::unique_lock` 可以在声明后的任意位置调用 ,
80+ 可以缩小锁的作用范围,提供更高的并发度。
5781
5882如果你用到了条件变量 `std::condition_variable::wait` 则必须使用 `std::unique_lock` 作为参数。
5983
@@ -62,121 +86,156 @@ void some_operation(const std::string &message) {
6286```cpp
6387#include <iostream>
6488#include <thread>
65- #include <mutex>
6689
67- std::mutex mtx ;
90+ int v = 1 ;
6891
69- void block_area() {
70- std::unique_lock<std::mutex> lock(mtx);
71- //...临界区
72- lock.unlock();
73- //...some other code
74- lock.lock(); // can lock again
75- }
76- int main() {
77- std::thread thd1(block_area);
92+ void critical_section(int change_v) {
93+ static std::mutex mtx;
94+ std::unique_lock<std::mutex> lock(mtx);
95+ // 执行竞争操作
96+ v = change_v;
97+ std::cout << v << std::endl;
98+ // 将锁进行释放
99+ lock.unlock();
78100
79- thd1.join();
101+ // 在此期间,任何人都可以抢夺 v 的持有权
80102
103+ // 开始另一组竞争操作,再次加锁
104+ lock.lock();
105+ v += 1;
106+ std::cout << v << std::endl;
107+ }
108+
109+ int main() {
110+ std::thread t1(critical_section, 2), t2(critical_section, 3);
111+ t1.join();
112+ t2.join();
81113 return 0;
82114}
83115```
84116
85- ## 7.3 std::future, std::packaged \_ task
117+ ## 7.3 期物
86118
87- ` std::future ` 则是提供了一个访问异步操作结果的途径,这句话很不好理解。为了理解这个特性,我们需要先理解一下在 C++11之前的多线程行为。
119+ 期物(Future)表现为 ` std::future ` ,它提供了一个访问异步操作结果的途径,这句话很不好理解。
120+ 为了理解这个特性,我们需要先理解一下在 C++11 之前的多线程行为。
88121
89- 试想,如果我们的主线程 A 希望新开辟一个线程 B 去执行某个我们预期的任务,并返回我一个结果。而这时候,线程 A 可能正在忙其他的事情,无暇顾及 B 的结果,所以我们会很自然的希望能够在某个特定的时间获得线程 B 的结果。
122+ 试想,如果我们的主线程 A 希望新开辟一个线程 B 去执行某个我们预期的任务,并返回我一个结果。
123+ 而这时候,线程 A 可能正在忙其他的事情,无暇顾及 B 的结果,
124+ 所以我们会很自然的希望能够在某个特定的时间获得线程 B 的结果。
90125
91- 在 C++11 的 ` std::future ` 被引入之前,通常的做法是:创建一个线程A,在线程A里启动任务 B,当准备完毕后发送一个事件,并将结果保存在全局变量中。而主函数线程 A 里正在做其他的事情,当需要结果的时候,调用一个线程等待函数来获得执行的结果。
126+ 在 C++11 的 ` std::future ` 被引入之前,通常的做法是:
127+ 创建一个线程 A,在线程 A 里启动任务 B,当准备完毕后发送一个事件,并将结果保存在全局变量中。
128+ 而主函数线程 A 里正在做其他的事情,当需要结果的时候,调用一个线程等待函数来获得执行的结果。
92129
93- 而 C++11 提供的 ` std::future ` 简化了这个流程,可以用来获取异步任务的结果。自然地,我们很容易能够想象到把它作为一种简单的线程同步手段。
130+ 而 C++11 提供的 ` std::future ` 简化了这个流程,可以用来获取异步任务的结果。
131+ 自然地,我们很容易能够想象到把它作为一种简单的线程同步手段,即屏障(barrier)。
94132
95- 此外,` std::packaged_task ` 可以用来封装任何可以调用的目标,从而用于实现异步的调用。例如:
133+ 为了看一个例子,我们这里额外使用 ` std::packaged_task ` ,它可以用来封装任何可以调用的目标,从而用于实现异步的调用。
134+ 举例来说:
96135
97136``` cpp
98137#include < iostream>
99138#include < future>
100139#include < thread>
101140
102- int main ()
103- {
141+ int main () {
104142 // 将一个返回值为7的 lambda 表达式封装到 task 中
105143 // std::packaged_task 的模板参数为要封装函数的类型
106144 std::packaged_task<int()> task([](){return 7;});
107- // 获得 task 的 future
145+ // 获得 task 的期物
108146 std::future<int> result = task.get_future(); // 在一个线程中执行 task
109- std::thread (std::move (task)).detach(); std::cout << "Waiting...";
110- result.wait();
147+ std::thread (std::move (task)).detach();
148+ std::cout << "waiting...";
149+ result.wait(); // 在此设置屏障,阻塞到期物的完成
111150 // 输出执行结果
112- std::cout << "Done!" << std:: endl << "Result is " << result.get() << '\n';
151+ std::cout << "done!" << std:: endl << "future result is " << result.get() << std::endl;
152+ return 0;
113153}
114154```
115155
116156在封装好要调用的目标后,可以使用 ` get_future() ` 来获得一个 ` std::future ` 对象,以便之后实施线程同步。
117157
118- ## 7.4 std::condition_variable
158+ ## 7.4 条件变量
119159
120- ` std::condition_variable ` 是为了解决死锁而生的。当互斥操作不够用而引入的。比如,线程可能需要等待某个条件为真才能继续执行,而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。所以,` condition_variable ` 实例被创建出现主要就是用于唤醒等待线程从而避免死锁。` std::condition_variable ` 的 ` notify_one() ` 用于唤醒一个线程;` notify_all() ` 则是通知所有线程。下面是一个生产者和消费者模型的例子:
160+ 条件变量 ` std::condition_variable ` 是为了解决死锁而生,当互斥操作不够用而引入的。
161+ 比如,线程可能需要等待某个条件为真才能继续执行,
162+ 而一个忙等待循环中可能会导致所有其他线程都无法进入临界区使得条件为真时,就会发生死锁。
163+ 所以,` condition_variable ` 实例被创建出现主要就是用于唤醒等待线程从而避免死锁。
164+ ` std::condition_variable ` 的 ` notify_one() ` 用于唤醒一个线程;
165+ ` notify_all() ` 则是通知所有线程。下面是一个生产者和消费者模型的例子:
121166
122167``` cpp
123- #include < condition_variable>
168+ #include < queue>
169+ #include < chrono>
124170#include < mutex>
125171#include < thread>
126172#include < iostream>
127- #include < queue >
128- # include < chrono >
173+ #include < condition_variable >
174+
129175
130- int main ()
131- {
132- // 生产者数量
176+ int main () {
133177 std::queue<int> produced_nums;
134- // 互斥锁
135- std::mutex m;
136- // 条件变量
137- std::condition_variable cond_var;
138- // 结束标志
139- bool done = false;
140- // 通知标志
141- bool notified = false;
142-
143- // 生产者线程
144- std::thread producer([&]() {
145- for (int i = 0; i < 5; ++i) {
146- std::this_thread::sleep_for (std::chrono::seconds (1));
147- // 创建互斥锁
148- std::unique_lock< std::mutex > lock(m);
149- std::cout << "producing " << i << '\n';
150- produced_nums.push(i);
151- notified = true;
152- // 通知一个线程
153- cond_var.notify_one();
154- }
155- done = true;
156- notified = true;
157- cond_var.notify_one();
158- });
159-
160- // 消费者线程
161- std::thread consumer ([ &] ( ) {
162- std::unique_lock< std::mutex > lock(m);
163- while (!done) {
164- while (!notified) { // 循环避免虚假唤醒
165- cond_var.wait(lock);
178+ std::mutex mtx;
179+ std::condition_variable cv;
180+ bool notified = false; // 通知信号
181+
182+ // 生产者
183+ auto producer = [&]() {
184+ for (int i = 0; ; i++) {
185+ std::this_thread::sleep_for (std::chrono::milliseconds (900));
186+ std::unique_lock< std::mutex > lock(mtx);
187+ std::cout << "producing " << i << std::endl;
188+ produced_nums.push(i);
189+ notified = true;
190+ cv.notify_all(); // 此处也可以使用 notify_one
166191 }
167- while (!produced_nums.empty()) {
168- std::cout << "consuming " << produced_nums.front() << '\n';
169- produced_nums.pop();
192+ };
193+ // 消费者
194+ auto consumer = [ &] ( ) {
195+ while (true) {
196+ std::unique_lock< std::mutex > lock(mtx);
197+ while (!notified) { // avoid spurious wakeup
198+ cv.wait(lock);
199+ }
200+ // 短暂取消锁,使得生产者有机会在消费者消费空前继续生产
201+ lock.unlock();
202+ std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 消费者慢于生产者
203+ lock.lock();
204+ while (!produced_nums.empty()) {
205+ std::cout << "consuming " << produced_nums.front() << std::endl;
206+ produced_nums.pop();
207+ }
208+ notified = false;
170209 }
171- notified = false;
172- }
173- });
210+ };
174211
175- producer.join();
176- consumer.join();
212+ // 分别在不同的线程中运行
213+ std::thread p(producer);
214+ std::thread cs[2];
215+ for (int i = 0; i < 2; ++i) {
216+ cs[i] = std::thread(consumer);
217+ }
218+ p.join();
219+ for (int i = 0; i < 2; ++i) {
220+ cs[i].join();
221+ }
222+ return 0;
177223}
178224```
179225
226+ 值得一提的是,在生产者中我们虽然可以使用 ` notify_one() ` ,但实际上并不建议在此处使用,
227+ 因为在多消费者的情况下,我们的消费者实现中简单放弃了锁的持有,这使得可能让其他消费者
228+ 争夺此锁,从而更好的利用多个消费者之间的并发。话虽如此,但实际上因为 ` std::mutex ` 的排他性,
229+ 我们根本无法期待多个消费者能真正意义上的并行消费队列的中生产的内容,我们仍需要粒度更细的手段。
230+
231+ ## 7.5 原子操作与内存一致性
232+
233+
234+
235+ ## 7.6 事务内存
236+
237+
238+
180239## 总结
181240
182241C++11 语言层提供了并发编程的相关支持,本节简单的介绍了 ` std::thread ` /` std::mutex ` /` std::future ` 这些并发编程中不可回避的重要工具。
0 commit comments