c++多线程之模版类lock_guard和unique_lock

c++多线程之模版类lock_guard和unique_lock

  • 简介
  • tag标签
  • lock_guard
  • unique_lock
  • 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 
#include 
#include 
#include        
#include          
#include           
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 unique_lock (mutex_type& m, const chrono::duration& rel_time) 阻塞直至锁定成功或超时
7.template unique_lock (mutex_type& m, const chrono::time_point& 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 unique_lock (mutex_type& m, const chrono::duration& rel_time),调用try_lock_for()。
  • 第7个template unique_lock (mutex_type& m, const chrono::time_point& abs_time),调用try_lock_until()。

为了更好理解上述构造函数,我们需要理解其几个成员函数。

  1. 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';
}
  1. 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';
}
  1. try_lock_for(const chrono::duration& 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";
}
  1. try_lock_until(const chrono::time_point& abs_time),类似try_lock_for(),但它传入的不是时间段而是相对于时钟的时间点,在abs_time时间点之前内成功获取锁返回true或超时未锁定返回false,同样经常与第4个构造函数一起使用。注意它使用的锁类型为timed_mutex
  2. owns_lock(),返回当前状态,若成功获取锁,返回true,否则返回false。

lock_guard与unique_lock的区别

lock_guard的使用比较简洁,使用起来相较unique_lock不太灵活,但开销小,而unique_lock的使用比较灵活但开销大,在满足需求的情况下尽量使用lock_guard。除此之外lock_guard不可在中途解锁,必须等待lock_guard对象生命周期结束。而unique_lock可随时加锁和解锁。

你可能感兴趣的:(多线程)