线程的互斥和同步(8)- C++11中的互斥锁和条件变量

1. 互斥锁

之前讲过使用Windows的API和Qt中如何创建和使用互斥锁。接下来,主要说明一下C++11中的互斥锁。
c++11中的互斥锁主要有如下几种:

互斥锁说明
mutex最基本的互斥锁,不可重入
timed_mutex具有超时机制的互斥锁
recursive_mutex可重入的互斥锁
recursive_timed_mutex结合 timed_mutex 和 recursive_mutex 特点的互斥量

他们都有加锁(lock)、尝试加锁(trylock)和解锁(unlock)的方法。

(1) 递归锁和非递归锁

这里说明一下 递归锁非递归锁

  • 递归锁 ,可重入锁。表示同一线程中,可以多次加锁,要解锁相应的次数才能够解锁。
  • 非递归锁 ,不可重入。表示如果多次加锁,会阻塞导致线程死锁。

Windows API CreateCreateMutex 和临界区都属于 递归锁 。
而Qt中的 QMutex 创建时默认是 非递归的锁 ,构造中传入参数 Recursive (可递归),如果不传则默认为 NonRecursive (非递归)。

接下来举一个例子来说明:

std::mutex g_mutex;
int g_number = 0;

void func2(void);
void func(void)
{
    g_mutex.lock();
    ++g_number;
    func2();
    g_mutex.unlock();
}

void func2(void)
{
    g_mutex.lock();
    ++g_number;
    g_mutex.unlock();
}

如果在线程中,调用函数 func() 就会引起线程 死锁 ,因为它加锁两次。
而使用 std::recursive_mutex 替换 std::mutex 就没有死锁的问题。
但是,这并不意味着,我们就需要使用 递归锁 ,使用 非递归锁 更容易定位我们程序中的问题。

(2) 互斥锁管理类

之前我们在讲 QMutex 的时候,同时讲解了RALL的 QMutexLocker ;C++11中也提供了类似的方法。
Qt中的互斥锁可参照:线程的互斥和同步(4)- Qt中的互斥锁(QMutex和QMutexLocker)

互斥锁管理类说明
lock_guard基于作用域的互斥量管理
unique_lock更加灵活的互斥量管理
  • lock_guard , 没有 lockunlock 仅提供构造时加锁,析构时解锁。
  • unique_lock , 允许延迟锁定、锁定的有时限尝试、递归锁定、所有权转移和与条件变量一同使用。

比如可以这么使用一个互斥锁管理器:

void func(void)
{
	std::unique_lock<std::mutex> locker(g_mutex, std::defer_lock);
	locker.lock();

	++g_number;
}

locker 在构造的时候可以指定为构造时加锁还是不加锁。
这里使用 std::defer_lock 表明构造的时候不加锁,使用 adopt_lock 表示构造的时候同时加锁。
析构时,根据当前的锁属性判断是否解锁,如果当前为加锁状态,析构时就解锁。

unique_lock 它满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 。
同时还有方法 swap (与另一 std::unique_lock 交换状态) 和 release (将关联互斥解关联而不解锁它)

关于 unique_lock 的更多使用,可参照:
https://zh.cppreference.com/w/cpp/thread/unique_lock


2. std::condition_variable

之前讲了使用Qt创建和使用条件变量,C++11中也提供了对于条件变量的操作方法
线程的互斥和同步(7)- Qt的条件变量QWaitCondition

它同样提供了wait和唤醒的方法

  • 等待:waitwait_forwait_until
  • 唤醒:notify_one (唤醒一个) 和 notify_all (唤醒全部)
这里需要注意的是 虚假唤醒
虚假唤醒 是指可能一个线程没有发送唤醒通知,但是该线程已经被唤醒。

通常防止虚假唤醒的做法是,使用一个 while 循环,如果是虚假唤醒,则继续等待:

std::mutex g_mutex;
std::condition_variable var;
bool g_pred = false;

void func(void)
{
    std::unique_lock<std::mutex> locker(g_mutex);

    // 循环等待,防止虚假唤醒
    while (!g_pred)
    {
        // 当唤醒时,g_pred 将设置为true
        var.wait(locker);
    }
}

也可以写成如下方式:

std::mutex g_mutex;
std::condition_variable var;
bool g_pred = false;

void func(void)
{
    std::unique_lock<std::mutex> locker(g_mutex);

    // 防止虚假唤醒
    var.wait(locker, [] {return g_pred; });
}

此处的 wait 函数的第二个参数,为一个仿函数(lambda表达式就是一个匿名仿函数), 如果返回值为 true 则忽略虚假唤醒;否则继续等待。

关于 std::condition_variable 更多介绍,可参考:
https://zh.cppreference.com/w/cpp/thread/condition_variable

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:线程的互斥和同步(9)- Qt中的读写锁QReadWriteLock