1. 用互斥保护共享数据
- 不变量(invariant):关于一个特定数据结构总为 true 的语句,比如
双向链表的两个相邻节点 A 和 B,A 的后指针一定指向 B,B 的前指针一定指向 A。有时程序为了方便会暂时破坏不变量,这通常发生于更新复杂数据结构的过程中,比如删除双向链表中的一个节点 N,要先让 N 的前一个节点指向 N 的后一个节点(不变量被破坏),再让 N 的后节点指向前节点,最后删除 N(此时不变量重新恢复)
- 线程修改共享数据时,就会发生破坏不变量的情况,此时如果有其他线程访问,就可能导致不变量被永久性破坏,这就是 race condition
- 如果线程执行顺序的先后对结果无影响,则为不需要关心的良性竞争。需要关心的是不变量被破坏时产生的 race condition
- C++ 标准中定义了 data race 的概念,指代一种特定的 race condition,即并发修改单个对象。data race 会造成未定义行为
- race condition 要求一个线程进行时,另一线程访问同一数据块,出现问题时很难复现,因此编程时需要使用大量复杂操作来避免 race condition
- 在访问一个数据结构前,先锁住与数据相关的互斥;访问结束后,在解锁互斥。通过构造 std::mutex的实例来创建互斥,调用成员函数lock()加锁,
调用unlock()解锁。C++标准库提供了类模板std::lock_guard,针对互斥实现了RAII手法:
在构造时互斥加锁,在析构时解锁。
#include <list>
#include <mutex>
#include <algorithm>
#include <iostream>
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int new_value) {
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(new_value);
}
bool list_contains(int value_to_find) {
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(),some_list.end(),value_to_find)
!= some_list.end();
}
int main() {
add_to_list(42);
std::cout << "contains(1)=" << list_contains(1)
<<", contains(42)=" << list_contains(42) << std::endl;
}
- C++17 提供了的 std::scoped_lock,它可以接受任意数量的 mutex,并将这些 mutex 传给 std::lock 来同时上锁,它会对其中一个 mutex 调用 lock(),对其他调用 try_lock(),若 try_lock() 返回 false 则对已经上锁的 mutex 调用 unlock(),然后重新进行下一轮上锁,标准未规定下一轮的上锁顺序,可能不一致,重复此过程直到所有 mutex 上锁,
从而达到同时上锁的效果。C++17 支持类模板实参推断,可以省略模板参数。
#include <iostream>
#include <mutex>
class A {
public:
[[maybe_unused]] static void lock() { std::cout << 1; }
[[maybe_unused]] static void unlock() { std::cout << 2; }
[[maybe_unused]] static bool try_lock() {
std::cout << 3;
return true;
}
};
class B {
public:
[[maybe_unused]] static void lock() { std::cout << 4; }
[[maybe_unused]] static void unlock() { std::cout << 5; }
[[maybe_unused]] static bool try_lock() {
std::cout << 6;
return true;
}
};
int main() {
A a;
B b;
{
std::scoped_lock l(a, b); // 16
std::cout << std::endl;
} // 23
}
- 一般 mutex 和要保护的数据一起放在类中,定义为 private 数据成员,而非全局变量,这样能让代码更清晰。但如果某个成员函数返回指向数据成员的指针或引用,则通过这个指针的访问行为不会被 mutex 限制,因此需要谨慎设置接口,确保 mutex 能锁住数据
#include <mutex>
class A {
public:
void f() {}
};
class B {
public:
A* get_data() {
std::lock_guard<std::mutex> l(m_);
return &data_;
}
private:
std::mutex m_;
A data_;
};
int main() {
B b;
A* p = b.get_data();
p->f(); // 未锁定 mutex 的情况下访问数据
}