mutex和unique_lock

  mutex是C++中的锁对象,unique_lock为C++中管理锁的对象,极大地方便了mutex的使用和维护。

mutex

  mutex是一个可锁定对象,旨在程序访问临界区的时候,防止其他线程此时进入临界区。互斥对象提供独占所有权并且不支持递归性(即线程不应锁定它已经拥有的互斥锁)。mutex类包含在头文件mutex中。

mutex的使用

  mutex主要使用的两个方法便是lock和unlock,这两个方法用于锁定和解锁mutex对象。一般情况下,mutex的使用如下所示:

std::mutex mtx;
int val = 0;
void thread_add() {
    mtx.lock();
    val++;
    mtx.unlock();
}
void thread_sub() {
    mtx.lock();
    val--;
    mtx.unlock();
}

mutex的缺点

  mutex在正常情况下都能够按照预期的结果执行。但是,如果进入临界区后由于某种原因线程提前中断返回,那么由于没有执行解锁方法,被加锁的临界区则不会被释放,其他线程永远无法进入临界区。

unique_lock

  unique_lock是管理具有唯一所有权的互斥对象的对象。在构造时(或通过移动分配给它),对象获得一个互斥对象,负责该对象的锁定和解锁操作。该对象支持两种状态:锁定和解锁。此类保证在销毁时将管理的互斥对象解锁(即使未显式调用解锁函数)。因此,它能够保证互斥对象在抛出异常的情况下正确解锁,克服了mutex使用中的缺点。但是,unique_lock对象不以任何方式管理互斥对象的生命周期:这要求互斥对象的持续时间应至少延长到管理它的unique_lock销毁。

unique_lock的构造函数

  unique_lock是一个模板类,需要使用Mutex类型作为模板参数。它有多个构造函数如下所示:

类型 函数原型
default (1) unique_lock() noexcept;
locking (2) explicit unique_lock (mutex_type& m);
try-locking (3) unique_lock (mutex_type& m, try_to_lock_t tag);
deferred (4) unique_lock (mutex_type& m, defer_lock_t tag) noexcept;
adopting (5) unique_lock (mutex_type& m, adopt_lock_t tag);
locking for (6) template <class Rep, class Period>
unique_lock (mutex_type& m, const chrono::duration<Rep,Period>& rel_time);
locking until (7) template <class Clock, class Duration>
unique_lock (mutex_type& m, const chrono::time_point<Clock,Duration>& abs_time);
copy [deleted] (8) unique_lock (const unique_lock&) = delete;
move (9) unique_lock (unique_lock&& x);

  (1)默认构造函数
  新创建的unique_lock对象不管理任何Mutex对象。
  (2)locking初始化
  新创建的unique_lock对象管理Mutex对象m,并尝试调用m.lock()对Mutex对象进行上锁,如果此时另外某个unique_lock对象已经管理了该Mutex对象m,则当前线程将会被阻塞。
  (3)try-locking初始化
  新创建的unique_lock对象管理Mutex对象m,并尝试调用m.try_lock()对Mutex对象进行上锁,但如果上锁不成功,并不会阻塞当前线程。
  (4)deferred初始化
  新创建的unique_lock对象管理Mutex对象m,但是在初始化的时候并不锁住Mutex对象。m应该是一个没有当前线程锁住的Mutex对象。
  (5)adopting初始化
  新创建的unique_lock对象管理Mutex对象m,m应该是一个已经被当前线程锁住的Mutex对象。(并且当前新创建的unique_lock对象拥有对锁(Lock)的所有权)。
  (6)locking一段时间(duration)
  新创建的unique_lock对象管理Mutex对象m,并试图通过调用m.try_lock_for(rel_time)来锁住Mutex对象一段时间(rel_time)。
  (7)locking直到某个时间点(time point)
  新创建的unique_lock对象管理Mutex对象m,并试图通过调用m.try_lock_until(abs_time)来在某个时间点(abs_time)之前锁住Mutex对象。
  (8)拷贝构造[被禁用]
  unique_lock对象不能被拷贝构造。
  (9)移动(move)构造
  新创建的unique_lock对象获得了由x所管理的Mutex对象的所有权(包括当前Mutex的状态)。调用move构造之后,x对象如同通过默认构造函数所创建的,就不再管理任何Mutex对象了。

unique_lock的成员函数

  主要使用到的成员函数为lock和unlock。
  其中lock函数的原型如下所示:

void lock();

  该函数将会锁定其管理的Mutex对象,如果该Mutex对象已经被其他线程占用,那么当前lock操作就会被阻塞直到可以获取到该互斥锁。当函数返回时,当前unique_lock对象就可以拥有该锁;如果加锁失败,则会抛出异常。
  unlock函数的原型如下所示:

void unlock();

  该函数将会解锁其管理的Mutex对象,并将其对Mutex对象的所有权状态改为false,如果调用unlock函数之前,当前unique_lock对象已经不再拥有该Mutex对象,那么函数会抛出异常。

unique_lock的使用

局部变量法

  使用局部unique_lock对象自动来管理对象,使用locking初始化构造函数自动创建并加锁,当离开局部的代码块时,局部对象被销毁自动解锁。示例如下:

#include<bits/stdc++.h>
#include<unistd.h>
#include<mutex>
using namespace std;
std::mutex mtx;
int num = 0;
void producer(){
    while (true) {
        sleep(1);
        std::unique_lock<std::mutex> lk(mtx);
        num++;
        cout << "[ producer Thread ] num : " << num << endl;
    }
}
void consumer(){
    while (true) {
        sleep(2);
        std::unique_lock<std::mutex> lk(mtx);
        num--;
        cout << "[ consumer Thread ] num : " << num << endl;
    }
}
int main(){
    srand(time(nullptr));
    thread add(producer);
    thread sub(consumer);
    add.join();
    sub.join();
    return 0;
}

  执行之后的部分结果如下所示:

[ producer Thread ] num : 1
[ consumer Thread ] num : 0
[ producer Thread ] num : 1
[ producer Thread ] num : 2
[ consumer Thread ] num : 1
[ producer Thread ] num : 2
[ producer Thread ] num : 3
[ consumer Thread ] num : 2
[ producer Thread ] num : 3
[ producer Thread ] num : 4
[ consumer Thread ] num : 3
[ producer Thread ] num : 4
...

手动加锁和解锁

  可以使用deferred初始化构造函数创建对象,然后使用lock和unlock方法实现加锁和解锁过程。其示例如下所示:

#include<bits/stdc++.h>
#include<unistd.h>
#include<mutex>
using namespace std;
std::mutex mtx;
int num = 0;
void producer(){
    //defer_lock初始化
    std::unique_lock<std::mutex> lk(mtx, std::defer_lock);
    while (true) {
        sleep(1);
        lk.lock();          //手动加锁
        num++;
        cout << "[ producer Thread ] num : " << num << endl;
        lk.unlock();        //手动解锁
    }
}
void consumer(){
    std::unique_lock<std::mutex> lk(mtx, std::defer_lock);
    while (true) {
        sleep(2);
        lk.lock();
        num--;
        cout << "[ consumer Thread ] num : " << num << endl;
        lk.unlock();
    }
}
int main(){
    srand(time(nullptr));
    thread add(producer);
    thread sub(consumer);
    add.join();
    sub.join();
    return 0;
}

  执行之后的部分结果如下所示:

[ producer Thread ] num : 1
[ consumer Thread ] num : 0
[ producer Thread ] num : 1
[ producer Thread ] num : 2
[ consumer Thread ] num : 1
[ producer Thread ] num : 2
[ producer Thread ] num : 3
[ consumer Thread ] num : 2
[ producer Thread ] num : 3
[ producer Thread ] num : 4
[ consumer Thread ] num : 3
[ producer Thread ] num : 4
[ producer Thread ] num : 5
···

  可以看出两种的结果是一致的,两种使用方法性能上的区别在于局部变量的创建和销毁可能会有更大的开销。

unique_lock使用的注意事项

  在日常的开发中更多的使用到了unique_lock对互斥锁进行加锁,踩到了一个平时没有留意的坑。当使用构造局部变量进行加锁的时,编写代码一定要留意sleep的使用位置。
  我们先来看看以下代码的问题:

#include <bits/stdc++.h>
#include <unistd.h>
#include <mutex>
using namespace std;
mutex mtx;
void printHelloWorld() {
    while (true) {
        unique_lock<mutex> lck(mtx);
        cout << "Hello World" << endl;
        sleep(1);
    }
}
void printHelloCPP() {
    while (true) {
        unique_lock<mutex> lck(mtx);
        cout << "Hello CPP" << endl;
        sleep(1);
    }
}
int main() {
    thread printHelloCPP_thread(printHelloCPP);
    thread printHelloWorld_thread(printHelloWorld);
    printHelloCPP_thread.join();
    printHelloWorld_thread.join();
    return 0;
}

  以上代码创建了两个线程分别互斥的进行输出,并且每次的输出都会等待1秒钟。但是当真正运行这段代码时,我们发现只有第一个创建的线程能够运行,输出结果如下:

Hello CPP
Hello CPP
Hello CPP
Hello CPP
Hello CPP
...

  分析原因,显然是printHelloWorld一直没有拿到互斥锁,因此导致只有printHelloCpp拿到了锁进行了输出。导致这一问题的原因在于:unique_lock在构造的时候自动加锁,在析构的时候解锁。这样一来,只有当一轮循环结束的时候才会释放锁,也就是出了循环体的时候。显然,sleep写在循环末尾的时候,还不会调用unique_lock的析构函数,此时依然处于加锁状态,并没有实现让权等待。
  为了解决这个问题,我们可以采取两种办法:
  1.将sleep提前到加锁之前:将sleep放在加锁之前,那么等待时就不会加锁,实现让权等待。
  2.使用手动加锁的写法,事先创建一个不加锁的unique_lock,然后在使用时手动加锁和解锁。
  这两种方法中我更倾向于第二种,因为第一种将sleep写在前面的方法和编写代码时候的习惯不太一致。一般情况下,我们都会选择在执行过操作之后进行等待,然后开启下一轮的循环,sleep提前的写法多多少少显得怪异。另外,第二种方法不会因为创建局部变量导致额外的开销。我们可以提前创建好所有的unique_lock类,然后在使用的时候手动进行加锁或者解锁,只不过忘记解锁也会带来问题。


当珍惜每一片时光~