当前位置: 首页 > 图灵资讯 > 技术篇> c++ 多线线程(二)-mutex

c++ 多线线程(二)-mutex

来源:图灵教育
时间:2023-06-02 09:29:45

一、介绍mutex头文件

Mutex 又称互斥量,又称互斥量,C++ 11中与 Mutex 声明相关类别(包括锁类型)和函数 <mutex> 所以如果需要使用头文件, std::mutex,就必须包含 <mutex> 头文件

c++ 多线线程(二)-mutex_#include

4种mutex

std::mutex,最基本的 Mutex 类。

std::recursive_mutex,递归 Mutex 类。

std::time_mutex,定时 Mutex 类。

std::recursive_timed_mutex,定时递归 Mutex 类。

Lock 类(两种)

std::lock_guard,与 Mutex RAII 相关,方便线程相互排斥上锁。

std::unique_lock,与 Mutex RAII 方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

其他类型

std::once_flag

std::adopt_lock_t

std::defer_lock_t

std::try_to_lock_t

函数

std::try_lock,试着同时锁定多个互斥量。

std::lock,可同时锁定多个相互斥量。

std::call_once,若需要同时调用多个线程的函数,call_once 可以保证多个线程只调用一次函数。

c++ 多线线程(二)-mutex_#include_02

std::mutex 介绍

下面以 std::mutex 为例介绍 C++11 互斥量中的用法。

std::mutex 是C++11 最基本的互斥量,std::mutex 对象提供了独家所有权的特征,即不支持递归地 std::mutex 对象上锁,而 std::recursive_lock 对相互斥量对象可以递归上锁。

std::mutex 的成员函数

1、构造函数,std::mutex不允许复制结构或复制结构 move 复制,最初产生的 mutex 对象是处于 unlocked 状态的。        2、lock(),调用线程将锁定相互排斥。线程调用函数会发生在下面 3 种类:(1). 如果目前互斥量没有被锁定,则调用线程将互斥量锁定,直到调用 在unlock之前,这个线程一直有这个锁。(2). 如果当前的相互排斥被其他线程锁定,则当前的调用线程被堵塞。(3). 如果当前相互排斥被当前调用线程锁定,则会产生死锁(deadlock)。       3、unlock(), 解锁,释放相互排斥的所有权。       4、try_lock(),试着锁定相互排斥,如果相互排斥被其他线程占据,当前线程就不会被阻塞。线程调用函数也会出现以下情况 3 (1). 如果当前的相互排斥量没有被其他线程占据,则该线程将相互排斥锁定,直到线程调用 unlock 释放互斥量。(2). 如果当前的相互排斥被其他线程锁定,则当前调用线程返回 false,而且不会被堵塞。(3). 如果当前相互排斥被当前调用线程锁定,则会产生死锁(deadlock)。

我们来看看mutex的用法:

// mutex example  #include <iostream>       // std::cout  #include <thread>         // std::thread  #include <mutex>          // std::mutex  std::mutex mtx;           // mutex for critical section  void print_block(int n, char c) {      // critical section (exclusive access to std::cout signaled by locking mtx):      mtx.lock();      for (int i = 0; i<n; ++i) { std::cout << c; }      std::cout << '\n';      mtx.unlock();  }  int main()  {      std::thread th1(print_block, 50, '*');      std::thread th2(print_block, 50, '$');      th1.join();      th2.join();      return 0;  }

c++ 多线线程(二)-mutex_初始化_03

如果不使用mutex,输出可能是这样的:线程之间存在乱码

c++ 多线线程(二)-mutex_互斥量_04

三、recursive_mutex介绍

std::recursive_mutex 与 std::mutex 同样,它也是一个可以上锁的对象,但和 std::mutex 不同的是,std::recursive_mutex 允许同一线程对互斥量多次上锁(即递归上锁),以获得对互斥量对象的多层所有权,std::recursive_mutex 释放相互排斥时,需要调用与锁层深度相同的次数 unlock(),可以理解为 lock() 次数和 unlock() 除此之外,次数相同,std::recursive_mutex 的特性和 std::mutex 大致相同。

四、time_mutex介绍

std::time_mutex 比 std::mutex 增加了两个成员函数,try_lock_for(),try_lock_until()。

try_lock_for 函数接受一个时间范围,这意味着如果线程在这段时间范围内没有锁定,它将被堵塞(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁,则直接返回 false),若其它线程在此期间释放锁,则该线程可获得相互排斥的锁,若超时(即在规定时间内仍未获得锁),则返回 false。

try_lock_until 函数接受一个时间点作为参数。如果线程在指定时间点到达前未获得锁,则阻塞。如果其他线程在此期间释放锁,线程可以获得相互排斥的锁。如果超时(即在指定时间内未获得锁),则返回 false。

下面的小例子说明了 std::time_mutex 的用法

#include <iostream>       // std::cout  #include <chrono>         // std::chrono::milliseconds  #include <thread>         // std::thread  #include <mutex>          // std::timed_mutex  std::timed_mutex mtx;  void fireworks() {    // waiting to get a lock: each thread prints "-" every 200ms:    while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {      std::cout << "-";    }    // got a lock! - wait for 1s, then this thread prints "*"    std::this_thread::sleep_for(std::chrono::milliseconds(1000));    std::cout << "*\n";    mtx.unlock();  }  int main ()  {    std::thread threads[10];    // spawn 10 threads:    for (int i=0; i<10; ++i)      threads[i] = std::thread(fireworks);    for (auto& th : threads) th.join();    return 0;  }

c++ 多线线程(二)-mutex_初始化_05

五、std::recursive_timed_mutex介绍

和 std:recursive_mutex 与 std::mutex 关系是一样的,std::recursive_timed_mutex 特性也可以从 std::timed_mutex 推导出来,感兴趣的同鞋可以自己查阅

六、lock类介绍

(1)std::lock_guard 介绍 std::lock_gurad 是 C++11 中定义的模板类。定义如下:             template <class Mutex> class lock_guard;   lock_guard 对象通常用于管理锁(Lock)对象,所以和 Mutex RAII 相关性,方便线程对互斥量上锁,即在某个 lock_guard 在对象的声明周期内,其管理的锁对象将始终保持锁定状态; lock_guard 生命周期结束后,其管理的锁对象将被解锁(注:类似 shared_ptr 等待智能指针管理动态分配的内存资源)。   模板参数 Mutex 代表相互排斥类型,如 std::mutex 类型应该是基本的 BasicLockable 标准库中定义了几种基本类型和类型 BasicLockable 类型,分别 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex(以上四种类型已在上一篇博客中介绍)和 std::unique_lock(本文将在后续介绍 std::unique_lock)。(注:BasicLockable 对象类型只需要满足两种操作,lock 和 unlock,另外还有 Lockable 类型,在 BasicLockable 基于类型的新增 try_lock 操作,所以满足 Lockable 对象应支持三种操作:lock,unlock 和 try_lock;最后还有一个 TimedLockable 对象,在 Lockable 在类型的基础上增加了新的类型 try_lock_for 和 try_lock_until 两种操作,一种是满意的 TimedLockable 对象应支持五种操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。     在 lock_guard 在对象结构中,传入 Mutex 对象(即它管理的 Mutex 对象)将被当前线程锁定。lock_guard 当物体被分析时,它管理它 Mutex 对象会自动解锁,因为程序员不需要手动调用 lock 和 unlock 对 Mutex 锁定和解锁操作,所以这也是最简单、最安全的锁定和解锁方式,特别是在程序抛出异常后被锁定 Mutex 对象可以正确解锁,极大地简化了程序员的编写和编写 Mutex 相关异常处理代码。    值得注意的是,值得注意的是,lock_guard 对象不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某一点上 lock_guard 在对象的声明周期内,其管理的锁对象将始终保持锁定状态; lock_guard 生命周期结束后,它管理的锁对象将被解锁。

// lock_guard example  #include <iostream>       // std::cout  #include <thread>         // std::thread  #include <mutex>          // std::mutex, std::lock_guard  #include <stdexcept>      // std::logic_error  std::mutex mtx;  void print_even (int x) {    if (x%2==0) std::cout << x << " is even\n";    else throw (std::logic_error("not even"));  }  void print_thread_id (int id) {    try {      // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:      std::lock_guard<std::mutex> lck (mtx);      print_even(id);    }    catch (std::logic_error&) {      std::cout << "[exception caught]\n";    }  }  int main ()  {    std::thread threads[10];    // spawn 10 threads:    for (int i=0; i<10; ++i)      threads[i] = std::thread(print_thread_id,i+1);    for (auto& th : threads) th.join();    return 0;  }

std::lock_guard 构造函数 1、locking 初始化 lock_guard 对象管理 Mutex 对象 m,并且在结构上是对的 m 上锁(调用) m.lock())。 2、adopting初始化 lock_guard 对象管理 Mutex 对象 m,与 locking 初始化(1) 不同的是, Mutex 对象 m 已被当前线程锁定。 3、拷贝构造 lock_guard 对象的复制结构和移动结构(move construction)所有这些都被禁用,所以 lock_guard 对象不得复制或移动结构。

#include <iostream>       // std::cout  #include <thread>         // std::thread  #include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock  std::mutex mtx;           // mutex for critical section  void print_thread_id(int id) {      mtx.lock();      std::lock_guard<std::mutex> lck(mtx, std::adopt_lock);      std::cout << "thread #" << id << '\n';  }  int main()  {      std::thread threads[10];      // spawn 10 threads:      for (int i = 0; i<10; ++i)          threads[i] = std::thread(print_thread_id, i + 1);      for (auto& th : threads) th.join();      return 0;  }

在 print_thread_id 中,我们首先是对的 mtx 上锁操作(mtx.lock();),然后用 mtx 构造一个对象 lock_guard 对象(std::lock_guard lck(mtx, std::adopt_lock);),注意此时 Tag 参数为 std::adopt_lock,说明当前线程已经锁定,之后 mtx 对象的解锁操作交由 lock_guard 对象 lck 来管理,在 lck 生命周期结束后,mtx 对象会自动解锁。

lock_guard 最大的特点是安全易用。请看下面的例子(参考),在异常抛出时通过。 lock_guard 对象管理的 Mutex 能正确解锁。

#include <iostream>       // std::cout  #include <thread>         // std::thread  #include <mutex>          // std::mutex, std::lock_guard  #include <stdexcept>      // std::logic_error  std::mutex mtx;  void print_even(int x) {      if (x % 2 == 0) std::cout << x << " is even\n";      else throw (std::logic_error("not even"));  }  void print_thread_id(int id) {      try {          // using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:          std::lock_guard<std::mutex> lck(mtx);          print_even(id);      }      catch (std::logic_error&) {          std::cout << "[exception caught]\n";      }  }  int main()  {      std::thread threads[10];      // spawn 10 threads:      for (int i = 0; i<10; ++i)          threads[i] = std::thread(print_thread_id, i + 1);      for (auto& th : threads) th.join();      return 0;  }

(2)std::unique_lock 介绍 但是 lock_guard 最大的缺点也很简单,没有给程序员足够的灵活性,所以,C++11 标准中定义了另一个和 Mutex RAII 相关类 unique_lock,该类与 lock_guard 类似地,线程对互斥量上锁也很方便,但它提供了更好的上锁和解锁控制。

顾名思义,unique_lock 对象以独占所有权的形式独占所有权( unique owership)管理 mutex 对象的锁定和解锁操作,所谓的独家所有权,是没有其他的 unique_lock 对象同时拥有一个 mutex 对象的所有权。     构造(或移动)(move)赋值)时,unique_lock 对象需要传递一个 Mutex 作为其参数,新创建的对象 unique_lock 对象负责传输 Mutex 锁定和解锁对象。      std::unique_lock 当它自己的分析时,对象也可以保证它管理的对象 Mutex 即使没有显式调用,对象也能正确解锁(即使没有显式调用) unlock 函数)。因此,和 lock_guard 同样,这也是一种简单、安全的锁定和解锁方法,特别是在程序抛出异常后被锁定 Mutex 对象可以正确解锁,大大简化了程序员的编写和 Mutex 相关异常处理代码。        值得注意的是,值得注意的是,unique_lock 对象也不负责管理 Mutex 对象的生命周期,unique_lock 对象只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某一点上 unique_lock 在对象的声明周期内,其管理的锁对象将始终保持锁定状态; unique_lock 生命周期结束后,它管理的锁对象将被解锁,这一点和 lock_guard 类似,但 unique_lock 为程序员提供更多的自由,我将在以下内容中介绍给您 unique_lock 的用法。         另外,与 lock_guard 模板参数相同 Mutex 代表相互排斥类型,如 std::mutex 类型应该是基本的 BasicLockable 标准库中定义了几种基本类型和类型 BasicLockable 类型,分别 std::mutex, std::recursive_mutex, std::timed_mutex,std::recursive_timed_mutex (以上四种类型已在上一篇博客中介绍)和 std::unique_lock(本文将在后续介绍 std::unique_lock)。(注:BasicLockable 对象类型只需要满足两种操作,lock 和 unlock,另外还有 Lockable 类型,在 BasicLockable 基于类型的新增 try_lock 操作,所以满足 Lockable 对象应支持三种操作:lock,unlock 和 try_lock;最后还有一个 TimedLockable 对象,在 Lockable 在类型的基础上增加了新的类型 try_lock_for 和 try_lock_until 两种操作,一种是满意的 TimedLockable 对象应支持五种操作:lock, unlock, try_lock, try_lock_for, try_lock_until)。std::unique_lock 构造函数

std::unique_lock 相对而言,构造函数的数量 std::lock_guard 多,一方面也是因为 std::unique_lock 结构更加灵活,从而更加灵活 std::unique_lock 对象可以接受额外的参数。总地来说,std::unique_lock 构造函数如下: (1) 默认构造函数 新创建的 unique_lock 对象不管理任何事情 Mutex 对象。 (2) locking 初始化 新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.lock() 对 Mutex 如果此时另一个物体被锁定,则对象将被锁定 unique_lock 对象已经管理好了 Mutex 对象 m,当前线程将被堵塞。 (3) try-locking 初始化 新创建的 unique_lock 对象管理 Mutex 对象 m,并尝试调用 m.try_lock() 对 Mutex 对象锁定,但如果锁定不成功,则不会阻塞当前线程。 (4) deferred 初始化 新创建的 unique_lock 对象管理 Mutex 对象 m,但在初始化时并没有被锁定 Mutex 对象。 m 应该是没有当前线程锁定的 Mutex 对象。 (5) adopting 初始化 新创建的 unique_lock 对象管理 Mutex 对象 m, m 应该是已经被当前线程锁定的 Mutex 对象。(以及目前新创建的 unique_lock 对象有锁(Lock)所有权)。 (6) locking 一段时间(duration) 新创建的 unique_lock 对象管理 Mutex 对象 m,并试图通过调用 m.try_lock_for(rel_time) 来锁住 Mutex 对象有一段时间(rel_time)。 (7) locking 直到某个时间点(time point) 新创建的 unique_lock 对象管理 Mutex 通过调用对象m,试图调用对象m m.try_lock_until(abs_time) 来到某个时间点(abs_time)之前锁住 Mutex 对象。 (8) 拷贝构造 [被禁用] unique_lock 对象不能复制结构。 (9) 移动(move)构造 新创建的 unique_lock 对象得到了理由 x 所管理的 Mutex 对象的所有权(包括当前对象的所有权) Mutex 的状态)。调用 move 构造之后, x 如果对象是通过默认构建函数创建的,则不再管理任何事情 Mutex 对象了。 综上所述,由 (2) 和 (5) 创建的 unique_lock 对象通常都有 Mutex 对象的锁。而通过 (1) 和 (4) 创建的不会有锁。通过 (3),(6) 和 (7) 创建的 unique_lock 对象,则在 lock 成功时获得锁。

关于unique_关于unique_lock 请参见下面的例子

#include <iostream>       // std::cout  #include <thread>         // std::thread  #include <mutex>          // std::mutex, std::lock, std::unique_lock  // std::adopt_lock, std::defer_lock  std::mutex foo, bar;  void task_a() {      std::lock(foo, bar);         // simultaneous lock (prevents deadlock)      std::unique_lock<std::mutex> lck1(foo, std::adopt_lock);      std::unique_lock<std::mutex> lck2(bar, std::adopt_lock);      std::cout << "task a\n";      // (unlocked automatically on destruction of lck1 and lck2)  }  void task_b() {      // foo.lock(); bar.lock(); // replaced by:      std::unique_lock<std::mutex> lck1, lck2;      lck1 = std::unique_lock<std::mutex>(bar, std::defer_lock);      lck2 = std::unique_lock<std::mutex>(foo, std::defer_lock);      std::lock(lck1, lck2);       // simultaneous lock (prevents deadlock)      std::cout << "task b\n";      // (unlocked automatically on destruction of lck1 and lck2)  }  int main()  {      std::thread th1(task_a);      std::thread th2(task_b);      th1.join();      th2.join();      return 0;  }