C++互斥锁介绍

986 阅读4分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

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

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

分类

<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;
}

运行结果如下:

Untitled

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

小结

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