c++ mutex 分析

本贴最后更新于 266 天前,其中的信息可能已经时移世异

在我们写多线程程序时,有些资源需要保证在同一时间内不会被多个线程访问和操作,这时候通常的做法是使用锁,具体的,对资源进行访问与操作的代码片段被称为临界区,通过锁的锁定,使得在同一时刻,只能有一个线程执行临界区的代码,这样保证了临界区代码的互斥性和原子性。

在 C++ 中,为我们提供了互斥锁,用于对临界区的锁定,基本的定义都在头文件中,下面我们来看看 C++ 中如何通过 mutex 实现对临界区的保护。

分类

头文件中提供了两种类型的同步工具,一种是直接的同步原语,包括:

  • mutex:独占,非递归的互斥锁
  • timed_mutex: 支持 timeout 的独占非递归的互斥锁
  • recursive_mutex:支持独占,递归的互斥锁
  • recursive_timed_mutex:支持 timeout 的独占递归的互斥锁

另一类是锁的包装类,包括:

  • lock_guard:实现严格基于作用域的互斥锁所有权包装器
  • unique_lock:实现可移动的互斥锁所有权包装器
  • scoped_lock:用于多个互斥锁的免死锁 RAII 封装器

通常来说,我们应该多用包装类,少用同步原语,包装类为我们提供了较为完善的锁管理机制,防止我们忘记调用 unlock 或者调用 lock 出错导致的死锁和内存泄漏。本文首先对这几个互斥锁进行介绍。

同步原语

std::mutex

mutex 类是用于保护被多个线程同时访问的共享数据的同步原语。

mutex 类有三个成员函数,分别是:

  • lock:获取锁的所有权,如果其他线程已经持有锁会阻塞调用线程
  • try_lock:尝试获取锁的所有权,如果其他线程已经持有锁会返回 false
  • unlock:释放锁的所有权

当调用线程调用 lock 方法或 try_lock 方法时,会获取到 mutex 的所有权,直到调用 unlock 方法释放锁;当一个线程已经获取了 mutex 的所有权时,其他线程再次调用 lock 时,会被阻塞在 lock 方法中,若是其他线程调用 try_lock 方法,则是会返回一个 false,表示有其他线程已经获取了锁。

std::timed_mutex

timed_mutex 类除了 mutex 类的 lock、try_lock 和 unlock 方法,提供了两个方法支持超时获取锁的所有权:

  • bool try_lock_for(const std::chrono::duration<Rep,Period>& timeout_duration)
  • bool try_lock_until(const std::chrono::time_point<Clock,Duration>& timeout_time)

当尝试获取锁的所有权超时之后,会返回 false

std::recursive_mutex

支持递归的互斥锁,当单个线程获取锁后,可以继续通过 try_lock 或 lock 方法进入临界区,在 recursive_mutex 中有个计数器用来记录 lock 的次数,当 unlock 调用相应的次数后,则会释放该递归锁,也可以称为可重入锁。

std::recursive_timed_mutex

recursive_timed_mutex 则是兼具 timed_mutex 和 recursive_mutex 的功能,支持超时退出的获取递归互斥锁。

示例

下面举个例子介绍上述几个 mutex 的使用。

#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

class Speaking {
 private:
  int a;
  std::mutex m;
  std::timed_mutex t_m;
  std::recursive_mutex r_m;
  std::recursive_timed_mutex r_t_m;

 public:
  Speaking() : a(0){};
  ~Speaking() = default;
  void speak_without_lock();
  void speak();
  void speak_timed_lock();
  void speak_recursive_lock();
  void speak_recursive_lock2();
  void speak_lock_without_recursive_lock();
  void speak_lock_without_recursive_lock2();
};

void Speaking::speak_without_lock() {
  std::cout << std::this_thread::get_id() << ": " << a << std::endl;
  a++;
}

void Speaking::speak() {
  m.lock();
  speak_without_lock();
  m.unlock();
}

void Speaking::speak_timed_lock() {
  if (t_m.try_lock_for(std::chrono::seconds(1))) {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    speak_without_lock();
    t_m.unlock();
  } else {
    std::cout << std::this_thread::get_id() << ": time_out" << std::endl;
  }
}

void Speaking::speak_recursive_lock() {
  r_m.lock();
  speak_recursive_lock2();
  r_m.unlock();
}

void Speaking::speak_recursive_lock2() {
  r_m.lock();
  speak_without_lock();
  r_m.unlock();
}

void Speaking::speak_lock_without_recursive_lock() {
  t_m.lock();
  speak_lock_without_recursive_lock2();
  t_m.unlock();
}

void Speaking::speak_lock_without_recursive_lock2() {
  if (t_m.try_lock_for(std::chrono::seconds(1))) {
    speak_without_lock();
    t_m.unlock();
  } else {
    std::cout << std::this_thread::get_id() << ": time_out" << std::endl;
  }
}

int main() {
  Speaking s;

  // std::cout << "speak without lock:" << std::endl;

  std::thread t1(&Speaking::speak_without_lock, &s);
  std::thread t2(&Speaking::speak_without_lock, &s);
  t1.join();
  t2.join();

  std::cout << "speak with lock:" << std::endl;
  std::thread t3(&Speaking::speak, &s);
  std::thread t4(&Speaking::speak, &s);
  t3.join();
  t4.join();

  std::cout << "speak with timed lock:" << std::endl;
  std::thread t_t1(&Speaking::speak_timed_lock, &s);
  std::thread t_t2(&Speaking::speak_timed_lock, &s);
  t_t1.join();
  t_t2.join();

  std::cout << "speak with recursive lock:" << std::endl;
  std::thread t_r1(&Speaking::speak_recursive_lock, &s);
  std::thread t_r2(&Speaking::speak_recursive_lock, &s);
  t_r1.join();
  t_r2.join();

  std::cout << "speak without recursive lock:" << std::endl;
  std::thread t_r3(&Speaking::speak_lock_without_recursive_lock, &s);
  t_r3.join();

  return 0;
}

运行结果如下:

image.png

  • 不使用 mutex 进行线程间同步时,a++ 由于不是原子的,会发生问题,
  • 使用 mutex 对 a++ 进行上锁之后就不会有线程间同步问题
  • 使用 timed_mutex 时,由于我们设置临界区中 sleep 了 2s,当另一个线程获取锁超时 1s 之后会自动返回 false
  • 验证 recursive_mutex 时,我们设置了会对 mutex 多次上锁,为了不让程序死锁,我们使用 timed_mutex 进行对照,可以看到 recursive_mutex 支持同一个线程反复上锁,而 timed_mutex 不支持重复上锁,在第二次上锁时超时了。

小结

本文对 c++ 的同步原语互斥锁进行了介绍,后续对互斥锁的包装类进行详细的介绍。

  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    106 引用 • 152 回帖
  • 线程
    120 引用 • 111 回帖 • 3 关注
3 操作
xiaowangzhixiao 在 2023-10-10 15:13:38 更新了该帖
xiaowangzhixiao 在 2021-09-24 21:54:18 更新了该帖
xiaowangzhixiao 在 2021-09-24 21:51:09 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...