一、介绍mutex头文件
Mutex 又称互斥量,又称互斥量,C++ 11中与 Mutex 声明相关类别(包括锁类型)和函数 <mutex>
所以如果需要使用头文件, std::mutex,就必须包含 <mutex>
头文件
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 可以保证多个线程只调用一次函数。
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; }
如果不使用mutex,输出可能是这样的:线程之间存在乱码
三、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; }
五、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; }