c++多线程之模版类lock_guard和unique_lock
简介
之前讲解互斥锁mutex中提到,mutex的上锁lock()和解锁unlock()操作必须成对使用,一旦我们在一个线程中获取一个mutex锁后忘记在线程结束时解锁,那么这个锁将一值处于上锁状态,导致其他需要获取这个锁的线程一直处于阻塞状态从而饿死。为了避免大家犯这种低级错误,也为了更好更灵活的使用锁,c++推出了模板类lock_guard和unique_lock。下面给大家具体讲解一下。
tag标签
在具体讲解之前需要给大家介绍几种与锁有关的tag参数,它作为构造函数的第二参数使用,此参数仅用于选择特定的构造函数,它是以下值之一。
值 | 描述 |
---|---|
no tag | 通过调用成员lock()在构造函数中获取锁 |
try_to_lock | 通过调用成员try_lock()在构造函数中获取锁 |
defer_lock | 在构造函数中不获取锁,假设此时在线程没有获取锁 |
adopt_lock | 采纳此时锁状态,假设此时该线程已经获取了锁 |
lock_guard
lock_guard的使用非常简洁,它在定义对象时需要使用一个mutex对象对其进行构造,并且在构造函数中对传入的mutex对象进行上锁,然后在其生命周期结束时自动调用析构函数中对锁进行解锁。这样可以防止我们上锁后忘记解锁所造成的死锁和饿死。看下面代码:
#include <iostream>
#include <stdlib.h>
#include <thread>
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
void print_thread_id(int id) {
std::lock_guard<std::mutex> lck(mtx);
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;
}
输出:
thread #1
thread #2
thread #3
thread #4
thread #5
thread #7
thread #6
thread #8
thread #9
thread #10
在创建lock_guard时可以传入参数std::adopt_lock,意思是采用当前锁定,注意前提条件是此时锁已经被锁定。我们更改上述代码中print_thread_id函数。
void print_thread_id(int id) {
mtx.lock();
std::lock_guard<std::mutex> lck(mtx,adopt_lock);
std::cout << "thread #" << id << '\n';
}
运行输出:
thread #1
thread #2
thread #3
thread #4
thread #5
thread #6
thread #7
thread #8
thread #9
thread #10
我们发现效果相同。下面给大家介绍另一个更灵活使用互斥锁的模板类unique_lock。
unique_lock
unique_lock的作用与lock_guard类似,但使用方式更灵活。我们先来看一下它的构造函数。
函数声明 | 说明 |
---|---|
1.默认构造函数 unique_lock() | 构造一个不管理任何锁的对象 |
2.explicit unique_lock (mutex_type& m) | 初始化时获取锁m |
3.unique_lock (mutex_type& m, try_to_lock_t tag) | 初始化时尝试锁定 |
4.unique_lock (mutex_type& m, defer_lock_t tag) | 初始化时不锁定 |
5.unique_lock (mutex_type& m, adopt_lock_t tag) | 初始化时维持当前锁定状态 |
6.template <class Rep, class Period>unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time) | 阻塞直至锁定成功或超时 |
7.template <class Clock, class Duration>unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time) | 阻塞直至锁定成功或时间点到达 |
8.unique_lock (unique_lock&& x) | 初始化一个对象并获取由x管理的互斥对象,包括它当前拥有的状态。x保持默认构造的状态 |
- 第1、2个构造函数使用与lock_guard一样,在这里不在重复。
- 第3个unique_lock (mutex_type& m,try_to_lock_t tag),使用时需要传入try_to_lock作为第二参数。初始化时调用m.try_lock(),防止其他线程长时间占用锁的情况下该线程一值阻塞,因此需要调用owns_lock()判断锁的状态。
- 第4个unique_lock (mutex_type& m, defer_lock_t tag),需要传入defer_lock作为第二参数。初始化时不获取锁。此构造函数的使用为了后续再进行获取锁的操作。
- 第5个unique_lock (mutex_type& m, adopt_lock_t tag),初始化时采纳当前锁的状态,前提是此时已经获得锁,否则会报错。
- 第6个template <class Rep, class Period> unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time),调用try_lock_for()。
- 第7个template <class Clock, class Duration> unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time),调用try_lock_until()。
为了更好理解上述构造函数,我们需要理解其几个成员函数。
- lock(),同调用互斥对象的lock(),我们经常与第4个构造函数一起使用。
void print_thread_id(int id) {
std::unique_lock<std::mutex> lck(mtx, std::defer_lock);
// critical section (exclusive access to std::cout signaled by locking lck):
lck.lock();
std::cout << "thread #" << id << '\n';
}
- try_lock(),同调用互斥对象的try_lock(),同样经常与第4个构造函数一起使用。
void print_star () {
std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
// print '*' if successfully locked, 'x' otherwise:
if (lck.try_lock())
std::cout << '*';
else
std::cout << 'x';
}
- try_lock_for(const chrono::duration<Rep,Period>& rel_time),类似try_lock(),但它不会直接返回,而是阻塞一段时间rel_time,在rel_time时间内成功获取锁返回true或超时未锁定返回false,同样经常与第4个构造函数一起使用。注意它使用的锁类型为timed_mutex。
void fireworks () {
std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);
// waiting to get a lock: each thread prints "-" every 200ms:
while (!lck.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";
}
- try_lock_until(const chrono::time_point<Clock,Duration>& abs_time),类似try_lock_for(),但它传入的不是时间段而是相对于时钟的时间点,在abs_time时间点之前内成功获取锁返回true或超时未锁定返回false,同样经常与第4个构造函数一起使用。注意它使用的锁类型为timed_mutex。
- owns_lock(),返回当前状态,若成功获取锁,返回true,否则返回false。
lock_guard与unique_lock的区别
lock_guard的使用比较简洁,使用起来相较unique_lock不太灵活,但开销小,而unique_lock的使用比较灵活但开销大,在满足需求的情况下尽量使用lock_guard。除此之外lock_guard不可在中途解锁,必须等待lock_guard对象生命周期结束。而unique_lock可随时加锁和解锁。