写在前面
C++并发编程实战的三四章内容的总结.
数据竞争(条件竞争)
看下面的多线程读写示例
#include <iostream>
#include <thread>
using namespace std;
int i{};
void f() {
int num = 10000;
for (int n{}; n < num; ++n) i = i + 1;
}
int main(int argc, char *argv[]) {
thread t1(f);
thread t2(f);
t1.join();
t2.join();
cout << i << endl; // 10719
return 0;
}
结果并不是 20000, 因为多线程会竞争读写访问的数据, 导致实际写入次数少于循环数量.
下面的互斥量, 条件变量等都可以用来解决线程数据的同步问题.
互斥
互斥量
mutex
lock_guard
互斥锁引发的死锁问题
发生死锁的四个条件
同时满足下面的四个条件才会发生死锁
- 互斥条件: 多个线程不能同时使用一个资源
- 持有并等待条件: 线程已经持有的线程被别的线程访问, 别的线程只能阻塞
- 不可剥夺条件: 自己使用完资源之前, 资源不会被别的线程获取
- 环路等待条件: 两个(或多个)线程获取资源的顺序存在环路
不用互斥锁也会死锁吗: 会的. 看下面的例子:
void bar(); void foo() { thread t1(bar); t1.join(); cout << "foo done" << endl; } void bar() { thread t2(foo); t2.join(); cout << "bar done" << endl; } int main() { bar(); foo(); return 0; }
互相等待对方结束的两个线程.
事实上这个程序会抛出异常
system_error
, 不会一直等待.what(): Resource temporarily unavailable
下面是使用 mutex 导致死锁的实例
单线程死锁
#include <mutex>
#include <iostream>
#include <thread>
using namespace std;
void t1();
void t2();
mutex m1, m2;
// 单线程死锁
void t1() {
lock_guard<std::mutex> l1(m1);
t2();
}
void t2() {
lock_guard<std::mutex> l2(m2);
t1();
}
int main(int argc, char *argv[]) {
t1();
t2();
return 0;
}
多线程死锁
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void ThreadA() {
mutex1.lock();
std::cout << "Thread A has mutex1" << std::endl;
// 休眠 1s,模拟线程 A 执行其他操作
std::this_thread::sleep_for(std::chrono::seconds(1));
// 尝试获取 mutex2 锁
std::cout << "Thread A is waiting for mutex2" << std::endl;
mutex2.lock();
std::cout << "Thread A has mutex2" << std::endl;
mutex2.unlock();
mutex1.unlock();
}
void ThreadB() {
mutex2.lock();
std::cout << "Thread B has mutex2" << std::endl;
// 休眠 1s,模拟线程 B 执行其他操作
std::this_thread::sleep_for(std::chrono::seconds(1));
// 尝试获取 mutex1 锁
std::cout << "Thread B is waiting for mutex1" << std::endl;
mutex1.lock();
std::cout << "Thread B has mutex1" << std::endl;
mutex1.unlock();
mutex2.unlock();
}
int main() {
std::thread threadA(ThreadA);
std::thread threadB(ThreadB);
threadA.join();
threadB.join();
return 0;
}
unique_lock
灵活加锁, 但是也耗费一定的资源. (相较于 lock_guard 而言)
不一定始终占有与之关联的互斥量.
具有更细粒度的控制级别
#include <thread>
#include <mutex>
class some_big_object {};
void swap(some_big_object& lhs, some_big_object& rhs);
class X {
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd) : some_detail(sd) {}
// 执行内部数据互换(避免死锁)
// c++17 scoped_lock version
friend void swap(X& lhs, X& rhs) {
if (&rhs == &lhs) return;
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
std::lock(lock_a, lock_b);
swap(lhs.some_detail, rhs.some_detail);
}
};
defer_lock
: 将互斥保留为未加锁状态.std::lock()
: 此时才开始上锁.
双检查锁初始化资源的问题
shared_lock
共享锁(读锁)
多个线程可以同时锁住同一个
std::shared_mutex
.
递归加锁
同步
条件变量(condition_variable)
利用条件变量等待事件完成
期值(future)
获取异步任务的返回值
#include <future>
#include <iostream>
using namespace std;
int find_ans() { return 42; }
int main(int argc, char *argv[]) {
// async 和 thread 的创建方式类似, 都是先传入函数签名(可调用对象), 然后传入函数参数
future<int> ans = std::async(find_ans);
cout << "ans is " << ans.get() << endl; // ans is 42
return 0;
}