背景
C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢,导致程序出现未定义或异常行为。通常的做法是在修改共享数据成员时进行加锁(mutex)。在使用锁时通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,但经常会出现lock之后离开共享成员操作区域时忘记unlock导致死锁的现象。针对以上的问题,C++11中引入了std::unique_lock与std::lock_guard两种数据结构。通过对lock和unlock进行一次封装,实现自动unlock的功能。
std::lock_guard
std::lock_guard是典型的RAII实现,功能相对简单。在构造函数中进行加锁,析构函数中进行解锁。下面是std::lock_guard的源码,也非常容易看出是RAII的设计。
template <typename _Mutex> class lock_guard { public: typedef _Mutex mutex_type; explicit lock_guard(mutex_type &__m) : _M_device(__m) { _M_device.lock(); // 构造加锁 } lock_guard(mutex_type &__m, adopt_lock_t) noexcept : _M_device(__m) { } ~lock_guard() { _M_device.unlock(); //析构解锁 } lock_guard(const lock_guard &) = delete; lock_guard &operator=(const lock_guard &) = delete; private: mutex_type &_M_device; };
std::unique_lock
std::unique_lock同样能够实现自动解锁的功能,但比std::lock_guard提供了更多的成员方法,更加灵活一点,相对来说占用空也间更大并且相对较慢,即需要付出更多的时间、性能成本。下面是其源码:
template <typename _Mutex> class unique_lock { public: typedef _Mutex mutex_type; unique_lock() noexcept : _M_device(0), _M_owns(false) { } explicit unique_lock(mutex_type &__m) : _M_device(std::__addressof(__m)), _M_owns(false) { lock(); _M_owns = true; } unique_lock(mutex_type &__m, defer_lock_t) noexcept : _M_device(std::__addressof(__m)), _M_owns(false) { } unique_lock(mutex_type &__m, try_to_lock_t) : _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock()) { } unique_lock(mutex_type &__m, adopt_lock_t) noexcept : _M_device(std::__addressof(__m)), _M_owns(true) { // XXX calling thread owns mutex } template <typename _Clock, typename _Duration> unique_lock(mutex_type &__m, const chrono::time_point<_Clock, _Duration> &__atime) : _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock_until(__atime)) { } template <typename _Rep, typename _Period> unique_lock(mutex_type &__m, const chrono::duration<_Rep, _Period> &__rtime) : _M_device(std::__addressof(__m)), _M_owns(_M_device->try_lock_for(__rtime)) { } ~unique_lock() { if (_M_owns) unlock(); } unique_lock(const unique_lock &) = delete; unique_lock &operator=(const unique_lock &) = delete; unique_lock(unique_lock &&__u) noexcept : _M_device(__u._M_device), _M_owns(__u._M_owns) { __u._M_device = 0; __u._M_owns = false; } unique_lock &operator=(unique_lock &&__u) noexcept { if (_M_owns) unlock(); unique_lock(std::move(__u)).swap(*this); __u._M_device = 0; __u._M_owns = false; return *this; } void lock() { if (!_M_device) __throw_system_error(int(errc::operation_not_permitted)); else if (_M_owns) __throw_system_error(int(errc::resource_deadlock_would_occur)); else { _M_device->lock(); _M_owns = true; } } bool try_lock() { if (!_M_device) __throw_system_error(int(errc::operation_not_permitted)); else if (_M_owns) __throw_system_error(int(errc::resource_deadlock_would_occur)); else { _M_owns = _M_device->try_lock(); return _M_owns; } } template <typename _Clock, typename _Duration> bool try_lock_until(const chrono::time_point<_Clock, _Duration> &__atime) { if (!_M_device) __throw_system_error(int(errc::operation_not_permitted)); else if (_M_owns) __throw_system_error(int(errc::resource_deadlock_would_occur)); else { _M_owns = _M_device->try_lock_until(__atime); return _M_owns; } } template <typename _Rep, typename _Period> bool try_lock_for(const chrono::duration<_Rep, _Period> &__rtime) { if (!_M_device) __throw_system_error(int(errc::operation_not_permitted)); else if (_M_owns) __throw_system_error(int(errc::resource_deadlock_would_occur)); else { _M_owns = _M_device->try_lock_for(__rtime); return _M_owns; } } void unlock() { if (!_M_owns) __throw_system_error(int(errc::operation_not_permitted)); else if (_M_device) { _M_device->unlock(); _M_owns = false; } } void swap(unique_lock &__u) noexcept { std::swap(_M_device, __u._M_device); std::swap(_M_owns, __u._M_owns); } mutex_type * release() noexcept { mutex_type *__ret = _M_device; _M_device = 0; _M_owns = false; return __ret; } bool owns_lock() const noexcept { return _M_owns; } explicit operator bool() const noexcept { return owns_lock(); } mutex_type * mutex() const noexcept { return _M_device; } private: mutex_type *_M_device; bool _M_owns; // XXX use atomic_bool }; template <typename _Mutex> inline void swap(unique_lock<_Mutex> &__x, unique_lock<_Mutex> &__y) noexcept { __x.swap(__y); }
从上面的源码对比非常容易看出std::unique_lock的实现比std::lock_guard复杂多了,提供了几个方法使编程更灵活,具体如下:
lock | locks the associated mutex |
try_lock | tries to lock the associated mutex, returns if the mutex is not available |
try_lock_for | attempts to lock the associated TimedLockable mutex, returns if the mutex has been unavailable for the specified time duration |
try_lock_until | tries to lock the associated TimedLockable mutex, returns if the mutex has been unavailable until specified time point has been reached |
unlock | unlocks the associated mutex |
以上方法,可以通过lock/unlock可以比较灵活的控制锁的范围,减小锁的粒度。通过try_lock_for/try_lock_until则可以控制加锁的等待时间,此时这种锁为乐观锁。
std::unique_lock与条件变量
这里举个并发消息队列的简单例子,是std::unique_lock与条件变量配合使用经典场景,并发消费共享成员变量m_queue的内容,且保证线程安全。
#include <queue> #include <mutex> #include <thread> #include <chrono> #include <memory> #include <condition_variable> typedef struct task_tag { int data; task_tag( int i ) : data(i) { } } Task, *PTask; class MessageQueue { public: MessageQueue(){} ~MessageQueue() { if ( !m_queue.empty() ) { PTask pRtn = m_queue.front(); delete pRtn; } } void PushTask( PTask pTask ) { std::unique_lock<std::mutex> lock( m_queueMutex ); m_queue.push( pTask ); m_cond.notify_one(); } PTask PopTask() { PTask pRtn = NULL; std::unique_lock<std::mutex> lock( m_queueMutex ); while ( m_queue.empty() ) { m_cond.wait_for( lock, std::chrono::seconds(1) ); } if ( !m_queue.empty() ) { pRtn = m_queue.front(); if ( pRtn->data != 0 ) m_queue.pop(); } return pRtn; } private: std::mutex m_queueMutex; std::condition_variable m_cond; std::queue<PTask> m_queue; }; void thread_fun( MessageQueue *arguments ) { while ( true ) { PTask data = arguments->PopTask(); if (data != NULL) { printf( "Thread is: %d\n", std::this_thread::get_id() ); printf(" %d\n", data->data ); if ( 0 == data->data ) //Thread end. break; else delete data; } } } int main( int argc, char *argv[] ) { MessageQueue cq; #define THREAD_NUM 3 std::thread threads[THREAD_NUM]; for ( int i=0; i<THREAD_NUM; ++i ) threads[i] = std::thread( thread_fun, &cq ); int i = 10; while( i > 0 ) { Task *pTask = new Task( --i ); cq.PushTask( pTask ); } for ( int i=0; i<THREAD_NUM; ++i) threads[i].join(); system( "pause" ); return 0; }
在示例代码中,我们使主线程向公共队列cq中Push任务,而其他的线程则负责取出任务并打印任务,由于std::cout并不支持并发线程安全,所以在打印任务时使用printf。主线程new出的任务,在其他线程中使用并销毁,当主线程发送data为0的任务时,则规定任务发送完毕,而其他的线程获取到data为0的任务后退出线程,data为0的任务则有消息队列负责销毁。整个消息队列使用标准模板库实现,现实跨平台。
std::unique_lock与std::lock_guard区别
上述例子中,std::unique_lock在线程等待期间解锁mutex,并在唤醒时重新将其锁定,而std::lock_guard却不具备这样的功能。所以std::unique_lock和std::lock_guard在编程应用中的主要区别总结如下:
- 如果只为保证数据同步,那么std::lock_guard完全够用;
- 如果除了同步还需要实现条件阻塞时,那么就需要用std::unique_lock。
原文地址:https://www.cnblogs.com/evenleee/p/11854619.html