Skip to content

Mutex

约 653 个字 42 行代码 预计阅读时间 4 分钟

  • 使用 mutex 在访问共享数据前加锁,访问结束后解锁。一个线程用特定的 mutex 锁定后,其他线程必须等待该线程的 mutex 解锁才能访问共享数据
  • std::lock_guard 可以自动处理 std::mutex 的加锁与解锁,它在构造时接受一个 mutex,并会调用 mutex.lock(),析构时会调用 mutex.unlock()
  • std::scoped_lock 它可以接受任意数量的 mutex,并将这些 mutex 传给 std::lock 来同时上锁,它会对其中一个 mutex 调用 lock(),对其他调用 try_lock(),若 try_lock() 返回 false,则对已经上锁的 mutex 调用 unlock(),然后重新进行下一轮上锁,标准未规定下一轮的上锁顺序,可能不一致,重复此过程直到所有 mutex 上锁,从而达到同时上锁的效果。
  • std::unique_lock 在构造时接受一个 mutex,并会调用 mutex.lock(),析构时会调用 mutex.unlock(),unique_lock 允许我们在中间过程去解锁并重新上锁。

Reader-Writer Mutex

  • 读锁:如果多个线程调用 shared_mutex.lock_shared(),多个线程可以同时读。
  • 写锁:一个写线程调用 shared_mutex.lock(),则读线程均会等待该写线程释放锁后才上锁。

Recursive Mutex

  • 它可以在一个线程上多次获取锁,但在其他线程获取锁之前必须释放所有的锁。
  • 多数情况下,如果需要递归锁,说明代码设计存在问题。比如一个类的每个成员函数都会上锁,一个成员函数调用另一个成员函数,就可能多次上锁,这种情况用递归锁就可以避免产生未定义行为。但显然这个设计本身是有问题的,更好的办法是提取其中一个函数作为 private 成员并且不上锁,其他成员先上锁再调用该函数

Initialize Protect

  • std::once_flag 和 std::call_once 来保证对某个操作只执行一次
C++
#include <iostream>
#include <mutex>
#include <thread>

class A {
public:
  void f() {
    std::call_once(flag_, &A::print, this);
    std::cout << 2;
  }

private:
  void print() { std::cout << 1; }

private:
  std::once_flag flag_;
};

int main() {
  A a;
  std::thread t1{&A::f, &a};
  std::thread t2{&A::f, &a};
  t1.join();
  t2.join();
}  // 122
  • static 局部变量在声明后就完成了初始化,这存在潜在的 race condition,如果多线程的控制流同时到达 static 局部变量的声明处,即使变量已在一个线程中初始化,其他线程并不知晓,仍会对其尝试初始化。为此,C++11 规定,如果 static 局部变量正在初始化,线程到达此处时,将等待其完成,从而避免了 race condition。只有一个全局实例时,可以直接用 static 而不需要 std::call_once
C++
template <typename T>
class Singleton {
public:
  static T& Instance();
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  Singleton() = default;
  ~Singleton() = default;
};

template <typename T>
T& Singleton<T>::Instance() {
  static T instance;
  return instance;
}