02-多线程基础——std::mutex

2020/07/03 cpp_concurrency 共 7310 字,约 21 分钟

资源竞争是并发编程不可回避的问题。对于资源竞争,要么设计无锁模式,要么老老实实的加锁排队等待。在Windows和Linux下有很多相关的系统API或对象进行线程同步,比如:

  • 互斥体
  • 信号量
  • 条件变量
  • 事件
  • 读写锁
  • 自旋锁
  • future:

但是,在C++11中只提供了互斥体和条件变量来进行线程同步,这里对互斥体进行简单介绍,条件变量另外介绍。同时,如果要实现信号量、读写锁、事件,可以很方便的使用互斥体和条件变量来实现,将在后续的《读写锁、自旋锁、信号量的CPP11实现》中讨论。

互斥体基本用法

互斥体,顾名思义,就是相互排斥的对象,也就是说当一个线程在访问(获取)某一对象的时候,其他线程需要等到其他线程访问完毕之后才能继续访问,也就是说同一时间只能有一个线程对该对象具有访问的权利。我们可以把互斥体想象为一把打开锁的钥匙,只有获得了钥匙才能打开锁拿到资源,当不再访问资源的时候就要把钥匙归还回去,让其他需要访问资源的线程能够获取这把钥匙继续访问资源。 C++11提供的std::mutex类就是这把钥匙,而它的成员函数lock和unlock则对应了加锁和解锁的动作。std::mutex就是那唯一的钥匙,所有需要访问资源的线程都需要先去获取锁(调用lock函数),当不再需要访问资源的时候就需要归还钥匙(调用unlock释放锁)。在一个线程拥有锁的情况下,其他线程都将阻塞在lock函数的调用上,因此为了并发效率的考虑,加锁的粒度要尽量小(也就是拿到货之后要尽快的释放锁)。

下面的代码很简单,创建一个线程池,调用add_job添加job,调用do_some_thing_on_job完成job,所有的job都存放在m_res_queue资源队列中,每个job都是独立的需要单独的线程去完成。因此对于queue的访问需要加锁——添加job的时候,从queue中获取job的时候都需要加锁。而加锁的粒度也应该尽量小,耗时的任务不能放在加锁中。

class work_job
{
public:
    work_job(){}
    ~work_job(){
        for(auto t& m_pool)
        {
            if(t.joinable())
            {
                t.join();
            }
        }
    }

    void do_some_thing_on_job(job_res &res){}

    void thread_task()
    {
        // 获取锁
        m_nutex.lock();
        job_res res = m_res_queue.front();  // 获取资源
        m_res_queue.pop();

        // 释放锁
        m_mutex.unlock();

        do_some_thing_on_job(res);
    }

    void create_thread()
    {
        int nThreadCount = 4;

        // 工作线程
        for(int i = 0; i < nThreadCount;++i)
        {
            m_pool.emplace_back(std::thread(thread_task));
        }
    }

    void add_job(job_res res)
    {
        // 获取锁
        m_nutex.lock();
        m_res_queue.push(res);

        // 释放锁
        m_mutex.unlock();
    }
    
    std::vector<std::thread> m_pool;
    std::queue<job_res> m_res_queue;
    std::mutex m_nutex;
};

互斥体的辅助类

一般实际工作中很少直接使用std::mutex调用lock和unlock,而是使用std::mutex的辅助类——std::lock_guard和std::unique_lock。

std::lock_guard

std::lock_guard没有太多要说的,它的主要功能是在构造的时候获取锁,在析构的时候释放锁。 注意: std::lock_guard 没有加锁、解锁的接口。

其实现大致如下:

template <typename _MutexType>
class lock_guard
{
public:
    explicit lock_guard(std::mutex &m):m_mutex(m)
    {
        m_mutex.lock();
    }
    ~lock_guard(){
        m_mutex.unlock();
    }

    // 禁止拷贝构造和拷贝赋值,也就是禁止lock_guard所守护的所在对象之间进行转移
    lock_guard(const lock_guard &) = delete;
    lock_guard & operator=(const lock_guard &) = delete;
private:
    _MutexType &m_mutex;
};

std::unique_lock

unique_lock和lock_guard的不同之处在于它不仅在构造的时候加锁在析构的时候释放锁,并且他可以在任意的地方调用lock和unlock,相对于lock_guard有更大的灵活性。 注意:

  • 看unique_lock的名字,就知道它是一个具有所有权概念的类。和unique_ptr一样,一旦一个unique_lock被赋值给另一个unique_lock(新对象),那么新的对象将释放锁的所有权,然后新对象将获得锁的所有权然后处于和原来的锁一样的状态,这一点可以看一下unique_lock的拷贝赋值函数的实现,很简单。
  • unique_lock如果已经加锁,然后再次尝试去加锁,将抛出异常!这一点可以参考其_Validate函数的实现。

以下为该类的实现:

template <class _Mutex>
class unique_lock { // whizzy class with destructor that unlocks mutex
public:
    using mutex_type = _Mutex;

    // CONSTRUCT, ASSIGN, AND DESTROY
    unique_lock() noexcept : _Pmtx(nullptr), _Owns(false) {}

    explicit unique_lock(_Mutex& _Mtx) : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // construct and lock
        _Pmtx->lock();
        _Owns = true;
    }

    unique_lock(_Mutex& _Mtx, adopt_lock_t)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(true) {} // construct and assume already locked

    unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
        : _Pmtx(_STD addressof(_Mtx)), _Owns(false) {} // construct but don't lock

    unique_lock(_Mutex& _Mtx, try_to_lock_t)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock()) {} // construct and try to lock

    template <class _Rep, class _Period>
    unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_for(_Rel_time)) {} // construct and lock with timeout

    template <class _Clock, class _Duration>
    unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_until(_Abs_time)) {} // construct and lock with timeout

    unique_lock(_Mutex& _Mtx, const xtime* _Abs_time)
        : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // try to lock until _Abs_time
        _Owns = _Pmtx->try_lock_until(_Abs_time);
    }

    unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) {
        _Other._Pmtx = nullptr;
        _Other._Owns = false;
    }

    unique_lock& operator=(unique_lock&& _Other) {
        if (this != _STD addressof(_Other)) {
            if (_Owns) {
                _Pmtx->unlock();
            }

            _Pmtx        = _Other._Pmtx;
            _Owns        = _Other._Owns;
            _Other._Pmtx = nullptr;
            _Other._Owns = false;
        }
        return *this;
    }

    ~unique_lock() noexcept {
        if (_Owns) {
            _Pmtx->unlock();
        }
    }

    unique_lock(const unique_lock&) = delete;
    unique_lock& operator=(const unique_lock&) = delete;

    void lock() { // lock the mutex
        _Validate();
        _Pmtx->lock();
        _Owns = true;
    }

    _NODISCARD bool try_lock() {
        _Validate();
        _Owns = _Pmtx->try_lock();
        return _Owns;
    }

    template <class _Rep, class _Period>
    _NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) {
        _Validate();
        _Owns = _Pmtx->try_lock_for(_Rel_time);
        return _Owns;
    }

    template <class _Clock, class _Duration>
    _NODISCARD bool try_lock_until(const chrono::time_point<_Clock, _Duration>& _Abs_time) {
        _Validate();
        _Owns = _Pmtx->try_lock_until(_Abs_time);
        return _Owns;
    }

    _NODISCARD bool try_lock_until(const xtime* _Abs_time) {
        _Validate();
        _Owns = _Pmtx->try_lock_until(_Abs_time);
        return _Owns;
    }

    void unlock() {
        if (!_Pmtx || !_Owns) {
            _Throw_system_error(errc::operation_not_permitted);
        }

        _Pmtx->unlock();
        _Owns = false;
    }

    void swap(unique_lock& _Other) noexcept {
        _STD swap(_Pmtx, _Other._Pmtx);
        _STD swap(_Owns, _Other._Owns);
    }

    _Mutex* release() noexcept {
        _Mutex* _Res = _Pmtx;
        _Pmtx        = nullptr;
        _Owns        = false;
        return _Res;
    }

    _NODISCARD bool owns_lock() const noexcept {
        return _Owns;
    }

    explicit operator bool() const noexcept {
        return _Owns;
    }

    _NODISCARD _Mutex* mutex() const noexcept {
        return _Pmtx;
    }

private:
    _Mutex* _Pmtx;
    bool _Owns;

    void _Validate() const { // check if the mutex can be locked
        if (!_Pmtx) {
            _Throw_system_error(errc::operation_not_permitted);
        }

        if (_Owns) {
            _Throw_system_error(errc::resource_deadlock_would_occur);
        }
    }
};

可以看到,unique_lock比lock_guard的更大灵活性不仅体现在有lock和unlock函数上,它还提供了更具灵活性的构造函数(在构造的时候决定是否加锁,构造的时候尝试加锁及尝试加锁的超时时间等)。你还可以在任意时刻使用release释放锁——该函数返回其所拥有的锁(假如有)并将自己的状态重置(并不改变原来锁的状态)。

std::mutex m;
auto ulock_1 = std::unique_lock(m);   // 初始化并加锁

auto ulock_2(ulock_1);    // m所有权从ulock_1转移到ulock_2

time_mutex

class timed_mutex { // class for timed mutual exclusion
public:
    timed_mutex() noexcept : _My_locked(0) {}

    timed_mutex(const timed_mutex&) = delete;
    timed_mutex& operator=(const timed_mutex&) = delete;

    void lock() { // lock the mutex
        unique_lock<mutex> _Lock(_My_mutex);
        while (_My_locked != 0) {
            _My_cond.wait(_Lock);
        }

        _My_locked = UINT_MAX;
    }

    _NODISCARD bool try_lock() noexcept { // try to lock the mutex
        lock_guard<mutex> _Lock(_My_mutex);
        if (_My_locked != 0) {
            return false;
        } else {
            _My_locked = UINT_MAX;
            return true;
        }
    }

    void unlock() { // unlock the mutex
        {
            // The lock here is necessary
            lock_guard<mutex> _Lock(_My_mutex);
            _My_locked = 0;
        }
        _My_cond.notify_one();
    }

    template <class _Rep, class _Period>
    _NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) { // try to lock for duration
        return try_lock_until(chrono::steady_clock::now() + _Rel_time);
    }

    template <class _Time>
    bool _Try_lock_until(_Time _Abs_time) { // try to lock the mutex with timeout
        unique_lock<mutex> _Lock(_My_mutex);
        if (!_My_cond.wait_until(_Lock, _Abs_time, _UInt_is_zero{_My_locked})) {
            return false;
        }

        _My_locked = UINT_MAX;
        return true;
    }

    template <class _Clock, class _Duration>
    _NODISCARD bool try_lock_until(
        const chrono::time_point<_Clock, _Duration>& _Abs_time) { // try to lock the mutex with timeout
        return _Try_lock_until(_Abs_time);
    }

    _NODISCARD bool try_lock_until(const xtime* _Abs_time) { // try to lock the mutex with timeout
        return _Try_lock_until(_Abs_time);
    }

private:
    mutex _My_mutex;
    condition_variable _My_cond;
    unsigned int _My_locked;
};

嵌套锁——recursive_mutex

C++标准库提供了std::recursive_mutex类,其功能与std::mutex类似,不同的是你可以在同一线程的单个实例上多次进行加锁操作。一个线程在获得嵌套锁之前,其他线程需要先释放掉该嵌套锁的所有权,一旦获得所有权那么你可以多次进行加锁操作,但是要释放锁那么你加锁几次就应该解锁一次。

大多数情况下,如果你要使用嵌套锁,那么你就应该考虑是否需要重新设计你的程序,因为嵌套锁带来的好处远远小于其所带来的坏处,此处不表。

文档信息

Search

    Table of Contents