互斥量lock_guard
前面一片博文【C++11多线程入门教程】系列之互斥量mutex介绍了互斥锁mutex的基本用法与注意事项。但是,使用std::mutex时候会出现这样一种情况:
std::mutex mtx;
mtx.lock();
// do_something ......
mtx.unlock();
如果在mtx.lock()
时候,执行do_something
出现异常,那么将无法执行mtx.unlock()
操作,导致程序卡住。那么,这个时候该怎么办呢?
这个时候我们介绍lock_guard()类模板来解决这个问题,先看下std::lock_guard()类模板的声明:
// CLASS TEMPLATE lock_guard
template <class _Mutex>
class lock_guard { // class with destructor that unlocks a mutex
public:
using mutex_type = _Mutex;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { // construct but don't lock
}
~lock_guard() noexcept { // unlock
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
通过上面的声明可以清晰的看到,lock_guard类模板有两个构造函数(第一个构造函数在构造就上锁,另外一个并不上锁),拷贝构造函数与拷贝运算符禁止使用。同时,我们发现lock_guard类模板在构造函数里面进行lock()上锁,析构函数里面进行unlock()解锁。通过这样的机制,就算执行lock()之后代码区域产生异常,但是执行析构函数时候会自动unlock()解锁。不会导致卡死的情况发生。
std::lock_guard代码示例
下面的示例是错误代码,运行会导致卡住。如果需要运行正确,只需要将②注释掉,更换使用①。
#include <iostream>
#include <thread>
#include <mutex>
#include <stdexcept>
std::mutex mtx;
void print_even(int x)
{
if (0 == x % 2)
{
std::cout << x << " is even." << std::endl;
}
else
{
throw (std::logic_error("not even"));
}
}
void print_thread_id(int id)
{
try
{
//std::lock_guard<std::mutex> lck(mtx); // ①
mtx.lock(); // ②
print_even(id);
mtx.unlock(); // ③
}
catch (std::logic_error&)
{
std::cout << "[exception caught]" << std::endl;
}
}
int main(void)
{
std::thread threads[10];
for (int i = 0; i < 10; ++i)
{
threads[i] = std::thread(print_thread_id, i + 1);
}
for (auto& th : threads)
th.join();
system("pause");
return 0;
}
关于lock_guard()能够有效处理mutex的lock()上锁到unlock()之间的出现异常不会卡住的问题,那么lock_guard()是否能够解决死锁问题呢?明显是不可以的。我们看下面的测试代码:
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
#include <stdexcept>
class Tickets
{
public:
// 订票线程
void book_tickets()
{
for (int i = 0; i < 1000000; i++)
{
std::cout << "订票开始,该线程执行:" << i << std::endl;
//mtx_tickets_0.lock(); // ①
//mtx_tickets_1.lock(); // ①
std::lock_guard<std::mutex> lck0(mtx_tickets_0); ⑦
std::lock_guard<std::mutex> lck1(mtx_tickets_1);
tickets_.push_back(i);
//mtx_tickets_0.unlock(); // ②
//mtx_tickets_1.unlock(); // ②
}
std::cout << "订票线程执行完毕." << std::endl;
}
// 出票线程
void issue_tickets()
{
for (int i = 0; i < 1000000; i++)
{
if (!tickets_.empty()) // 订票量不为空
{
std::lock_guard<std::mutex> lck1(mtx_tickets_1); / ⑧
std::lock_guard<std::mutex> lck0(mtx_tickets_0); /
//mtx_tickets_1.lock(); // ③
//mtx_tickets_0.lock(); // ③
int ticket = tickets_.front();
tickets_.pop_front();
//mtx_tickets_1.unlock(); // ④
//mtx_tickets_0.unlock(); // ④
}
else
{
std::cout << "订票队列里面为空:" << i << std::endl;
}
}
std::cout << "出票线程执行完毕." << std::endl;
}
private:
std::list<int> tickets_;
std::mutex mtx_tickets_0;
std::mutex mtx_tickets_1;
};
int main(void)
{
Tickets tickets;
std::thread bookTickets(&Tickets::book_tickets, &tickets);
std::thread issueTickets(&Tickets::issue_tickets, &tickets);
bookTickets.join();
issueTickets.join();
std::cout << "主线程执行结束." << std::endl;
system("pause");
return 0;
}
上述代码是我们上一篇博文的订票与出票的mutex示例程序,我们换用lock_guard()来进行操作,发现还是会出现死锁卡住原因。其实不难理解:lock_guard()只是包装了mutex的lock()与unlock()分别位置构造与析构函数中。但是,如果上锁顺序不一致,仍然会出现死锁现象。解决方法是将代码中的⑦⑧上锁顺序一致即可。
小结
关于lock_guard()的一些小结:
- lock_guard()包装了mutex的lock()与unlock()机制,避免我们使用时候忘记unlock()的情况。当然,使用灵活性没有mutex强。
- lock_guard()的上锁机制能够有效避免lock()上锁后异常发生在unlock()解锁前的卡死现象。
- lock_guard()还提供了一个构造但是不上锁的构造函数,通过下面的写法也能够避免忘记unlock()导致的异常。
mtx.lock();
std::lock_guard<std::mutex> lck(mtx, std::adopt_lock());
// do_something...
// 这里不需要unlock()操作,lock_guard()会帮助析构mtx
- lock_guard()虽然避免忘记unlock()的风险,但是只能够在离开作用于使用析构函数进行unlock()。没有单独的接口进行lock()与unlock(),不够灵活。
参考
http://www.cplusplus.com/reference/mutex/lock_guard/