std::unique_lock与std::lock_guard分析

背景

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

时间: 2024-10-10 18:44:33

std::unique_lock与std::lock_guard分析的相关文章

C++11 std::unique_lock与std::lock_guard区别及多线程应用实例

C++11 std::unique_lock与std::lock_guard区别及多线程应用实例 C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为.通常的做法是在修改共享数据成员的时候进行加锁--mutex.在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,进场会出现由于疏忽导致由于lock之后在离开共享成员操作区域时忘记unlock,导致死锁. 针对以上的问题,C++11中引

[C++11 并发编程] 08 - Mutex std::unique_lock

相对于std::lock_guard来说,std::unique_lock更加灵活,std::unique_lock不拥有与其关联的mutex.构造函数的第二个参数可以指定为std::defer_lock,这样表示在构造unique_lock时,传入的mutex保持unlock状态.然后通过调用std::unique_lock对象的lock()方法或者将将std::unique_lock对象传入std::lock()方法来锁定mutex. #include <mutex> class some

基于std::mutex std::lock_guard std::condition_variable 和std::async实现的简单同步队列

C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为.通常的做法是在修改共享数据成员的时候进行加锁--mutex.在使用锁的时候通常是在对共享数据进行修改之前进行lock操作,在写完之后再进行unlock操作,进场会出现由于疏忽导致由于lock之后在离开共享成员操作区域时忘记unlock,导致死锁. 针对以上的问题,C++11中引入了std::unique_lock与std::lock_guard两种数据结构.通过对lock和

使用std::lock 和 std::unique_lock来起先swap操作

在上面代码中std::unique_lock可以传进std::lock,因为std::unique_lock有unique_lock提借lock.try_lock.unlock成员函数. std::unique_lock有一个owner_lock函数来判断是否现在已经被锁定.你可以会说使用std::lock_guard可能稍微效率一点.但是std::unique_lock使用可以更灵活,一是可以延迟锁定,二是可以将lock的所有权传送给另一个scope. 原文地址:https://www.cnb

C++11学习笔记:std::move和std::forward源码分析

std::move和std::forward是C++0x中新增的标准库函数,分别用于实现移动语义和完美转发. 下面让我们分析一下这两个函数在gcc4.6中的具体实现. 预备知识 引用折叠规则: X& + & => X& X&& + & => X& X& + && => X& X&& + && => X&& 函数模板参数推导规则(右值引用参数部分):

关于std::thread以及std::condition_variable的一些细节备忘

也算是看过不少多线程相关的资料了,但是一直对于其中的一些细节没有太好的把握,比如std::thread线程真正开始运行的时机,比如join.detch等真正的作用. 跟着<Cplusplus Concurrency In Action_Practical Multithreading>又过了一遍相关的细节,下面记录一下一些个人所获得的收获. std::thread真正开始运行的时机 下面是我尝试写的一个基于条件变量和互斥量的生产者消费者模型的Demo,就从这里开始说起 #include<

C++11新特性应用--实现延时求值(std::function和std::bind)

说是延时求值,注意还是想搞一搞std::function和std::bind. 之前博客<C++11新特性之std::function>注意是std::function怎样实现回调函数. 如今就算是补充吧,再把std::bind进行讨论讨论. 何为Callable Objects? 就可以调用对象,比方函数指针.仿函数.类成员函数指针等都可称为可调用对象. 对象包装器 Function wrapper Class that can wrap any kind of callable eleme

C++ std::unordered_map使用std::string和char *作key对比

最近在给自己的服务器框架加上统计信息,其中一项就是统计创建的对象数,以及当前还存在的对象数,那么自然以对象名字作key.但写着写着,忽然纠结是用std::string还是const char *作key,哪个效率高些.由于这服务器框架业务逻辑全在lua脚本,在C++需要统计的对象没几个,其实用哪个没多大区别.我纠结的是,很久之前就知道这两者效率区别不大,但直到现在我都还没搞清楚为啥,于是写些代码来测试. V1版本的代码如下: #ifndef __MAP_H__ #define __MAP_H__

error LNK2005: “public: class std::vector&lt;class std::vector&lt;class std::vector&lt;float&gt;”

VS2010:error LNK2005: "public: class std::vector<class std::vector<class std::vector<class std::vector<float,class std::allocator<float> >,class std::allocator<class std::vector<float,class std::allocator<float> 如: Re