CPP

C++11 并发编程之二 - mutex

Posted by BitLines on March 2, 2018

C++11 并发编程之二 - mutex

mutex,顾名思义就是互斥变量,用来提供线程间同步的方法。c++ 11 的 mutex 库提供了 4 种互斥变量 mutex,两个管理锁资源的lock,和三个函数。

四种mutex分别是:

  • mutex 基础mutex
  • recursive_mutex 递归mutex
  • timed_mutex 计时mutex
  • recursive_timed_mutex 递归计时mutex

两种管理锁资源的lock分别是:

  • lock_guard 基础的RAII(Resource Acquisition Is Initialization)锁管理
  • unique_lock 包括lock_guard的所有功能,在此基础上提供了更灵活的锁管理功能,推荐使用

三个函数分别是:

  • try_lock 按顺序同时尝试对多个mutex加锁,失败返回第一个加锁失败的mutex的下标,成功返回-1
  • lock 按顺序同时对多个mutex加锁,阻塞等待直到获得所有mutex的锁
  • call_once 多次使用该方法调用函数,可以保证函数只被调用一次

下面依次对上面提到的内容做详细的介绍,我保证,看完了你就知道怎么用啦。

mutex

mutex 的声明类似下面这样

class mutex
{
public :
    // 底层实现句柄类型
    typedef some_handle_type native_handle_type;
public :
    // 默认构造
    mutex();
    // 禁止拷贝构造和移动构造
    mutex(const mutex&) = delete;
public :
    // 加锁,阻塞等待,直到获得锁
    void lock();
    // 尝试加锁,立即返回是否加锁成功
    bool try_lock();
    // 解锁。注意,互斥变量必须是被锁定状态,否则行为未定义
    void unlock();
    // 获取底层实现句柄
    native_handle_type native_handle();
};

看起来也比较简单,不详细介绍了。

recursive_mutex

recursive_mutex 提供的方法与mutex完全一样。与 mutex 的差别在于其递归的特性。具体来说就是,同一线程内,可以对 recursive_mutex 重复加锁,锁的释放次数应该和获得次数一样。使用场景可以是占有锁的函数的递归调用。

timed_mutex

timed_mutex 比 mutex 多添加了计时的特性,在获得锁的时候可以指定等待一段时间或者等待直到某个时间点。具体提供的方法来看下面

class timed_mutex
{
public :
    // 底层实现句柄类型
    typedef some_handle_type native_handle_type;
public :
    // 加锁,阻塞等待,直到获得锁
    void lock();
    // 尝试加锁,立即返回是否加锁成功
    bool try_lock();
    // 尝试加锁,阻塞等待直到超时,返回是否加锁成功
    template <class Rep, class Period>
    bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
    // 尝试加锁,阻塞等待直到超时,返回是否加锁成功
    template <class Clock, class Duration>
    bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
    // 解锁。注意,互斥变量必须是被锁定状态,否则行为未定义
    void unlock();
    // 获取底层实现句柄
    native_handle_type native_handle();
};

recursive_timed_mutex

recursive_timed_mutex 从名字就可以看出来是 timed_mutex 和 recursive_mutex 的结合体,其实真的就是这样子!不详细讲啦。

lock_guard

lock_guard 只有构造函数和析构函数,简单的RAII。其构造函数给互斥变量加锁,析构函数释放锁。并不负责互斥变量生命周期的管理。 lock_guard 接口如下:

template <typename Mutex>
class lock_guard
{
// 内部类型
public :
    typedef Mutex mutex_type;
// 构造函数
public :
    // 加锁构造。lock_guard 管理 m。 构造时,调用 m 的 lock方法,
    // 阻塞当前线程直到获得 m 的锁。
    explicit lock_guard (mutex_type& m); 
    // 适应构造。构造前,m 必须是已经被锁定的状态, 不再调用 lock 方法。
    lock_guard (mutex_type& m, adopt_lock_t tag);
    // 禁止拷贝构造和移动构造
    lock_guard (const lock_guard&) = delete;
// 析构函数
public :
    // 析构时,调用 m 的unlock方法。
    ~lock_guard();

};

对于上面的适应构造,为了不留坑在这里,还是举个例子保险,不要打我,我确实啰嗦了一点,请看代码。

#include <iostream>
#include <mutex>

using namespace std;

mutex mtx;

void thread_1()
{
    while (true)
    {   
        // 1. 把 mtx 锁定
        // 2. 使用适应构造
        mtx.lock();
        lock_guard<mutex> lck(mtx, adopt_lock);
        // do someting
    }   
}

void thread_2()
{
    while (true)
    {   
        // 直接使用加锁构造
        lock_guard<mutex> lck(mtx);
        // do someting
    }   
}

lock_guard 使用起来很简单。来,再看个例子巩固一下。我们结合 lock_guard 和 recursive_mutex, 使例子不那么单调。 (*\^▽^*)

#include <iostream>
#include <mutex>
#include <queue>

using namespace std;

class SyncQueue
{
public :
    explicit SyncQueue(size_t limit = 0) : mLimit(limit) {}
    ~SyncQueue() {}
public :
    bool Push(int x)
    {   // 获得锁
        lock_guard<recursive_mutex> lock(mMtx);
        if (mLimit && this->GetSize() >= mLimit) { return false; }
        mData.push(x);
        return true;
    }   
    bool Pop(int &x) 
    {   
        // 获得锁
        lock_guard<recursive_mutex> lock(mMtx);
        // 这里对 recursive_mutex 进行了二次加锁
        if (0 == this->GetSize()) { return false; }
        x = mData.front();
        mData.pop();
        return true;
    }   
    size_t GetSize()
    {   
        // 获得锁
        lock_guard<recursive_mutex> lock(mMtx);
        return mData.size();
    }   
private :
    queue<int> mData;
    recursive_mutex mMtx;
    size_t mLimit;
};

unique_lock

unique_lock 的作用和 lock_guard。 只不过提供了比 lock_guard 更灵活的 mutex 管理方法。lock_guard 只提供了最普通的RAII,也只有简单的构造函数和析构函数,unique_lock 除此之外,提供了 timed_mutex 的一些功能,可以指定等待一段时间或者等待直到某个时间点。看其名字,跟unique_ptr,类似,确实也有类似的功能,对一个mutex提供一个唯一管理的实例,并且可以进行移动拷贝和移动构造。但是 unique_lock 也不负责管理mutex的生命周期,要自己保证互斥变量的生命周期长于 unique_lock 。 废话不多说,看看接口就明白了

template <typename Mutex>
class unique_lock
{
// 内部类型
public :
    typedef Mutex mutex_type;
// 构造函数
public :
    // 加锁构造。lock_guard 管理 m。 构造时,调用 m 的 lock方法,
    // 阻塞当前线程直到获得 m 的锁。
    explicit unique_lock (mutex_type& m); 
    // 适应构造。构造前,m 必须是已经被锁定的状态, 不再调用 lock 方法。
    unique_lock (mutex_type& m, adopt_lock_t tag);
    // 禁止拷贝构造和移动构造
    unique_lock (const lock_guard&) = delete;
// 析构函数
public :
    // 析构时,如果 m 是被锁定的状态,则调用 m 的unlock方法。
    ~unique_lock();
// 加锁解锁
public :
    // 加锁,阻塞等待,直到获得锁
    void lock();
    // 尝试加锁,立即返回是否加锁成功
    bool try_lock();
    // 尝试加锁,阻塞等待直到超时,返回是否加锁成功
    template <class Rep, class Period>
    bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);
    // 尝试加锁,阻塞等待直到超时,返回是否加锁成功
    template <class Clock, class Duration>
    bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
    // 解锁。注意,互斥变量必须是被锁定状态,否则行为未定义
    void unlock();
// 修改方法
public :
    // 支持移动赋值
    unique_lock& operator= (unique_lock&& x) noexcept;
    // 禁止拷贝赋值
    unique_lock& operator= (const unique_lock&) = delete;
    // 和另一个 unique_lock 交换
    void swap (unique_lock& x) noexcept;
    // 返回管理的互斥变量的指针, 并且不再对其进行管理。
    mutex_type* release() noexcept;
// 观察方法
public :
    // 判断当前是否获得了锁。
    bool owns_lock() const nexept;
    // 同owns_lock。
    explicit operator bool() const noexcept;
    // 返回管理的互斥变量的指针
    mutex_type* mutex() const noexcept;
};

OK, 我知道你觉得一堆函数看起来很晕,不过耐心看完的话,是不是觉得功能很齐全。记不住没关系,用的时候去查查API就可以了。如果只是使用RAII的话,unique_lock 和 lock_guard 可以看做是等价的。所以上面给出的lock_guard的代码示例,对于 unique_lock 同样适用。这里就不再举例了。

外部函数


// 按顺序同时尝试对多个mutex加锁,失败返回第一个加锁失败的mutex的下标,成功返回-1。

template <class Mutex1, class Mutex2, class... Mutexes>
int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

// 按顺序同时对多个mutex加锁,阻塞等待直到获得所有mutex的锁
template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);

// 对同一个 once_flag 变量调用 call_once,保证 call_once 只被执行一次。调用 call_once 的效果是,
//  1. 如果没有其他线程已经对同一个 once_flag 正在调用或者调用完成 call_once,则执行 Fn 函数,并等待其执行完成
//  2. 如果已经有一个其他线程对同一个 once_flag调用完成 call_once,则不再执行 Fn 直接返回
//  3. 如果存在有一个其他线程对同一个 once_flag 正在执行 Fn 函数,则阻塞当前线程,直到其他线程执行Fn 完成。
// 眼前一亮,这简直就是自带单例功能嘛。
template <class Fn, class... Args>
void call_once (once_flag& flag, Fn&& fn, Args&&... args);

对 try_lock 和 lock 就不讲了,下面附上一段单例模式的示例代码,感受一下 call_once 函数有多么好用

#include <mutex>

using namespace std;

class Singleton
{
// 构造函数
private :
    // 禁止构造
    Singleton() {}
// 析构函数
public :
    ~Singleton() {}
// 静态方法
public :
    // 获取实例
    static Singleton * GetInstance();
// 私有方法
private :
    // 构造实例
    static void createInstance();
// 私有变量
private :
    static Singleton * sInstance;
    static once_flag sOnceFlag;
};

Singleton * Singleton::sInstance = nullptr;

once_flag Singleton::sOnceFlag;

void Singleton::createInstance()
{
    sInstance = new Singleton();
}

Singleton * Singleton::GetInstance()
{
    // 保证了该类的对象只被生成一次。
    call_once(sOnceFlag, createInstance);
    return sInstance;
}