Skip to content

互斥锁mutex

互斥锁是一种同步原语,用于协调多个线程对共享资源的访问。互斥锁的作用是保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待互斥锁释放后才能访问。在多线程编程中,多个线程可能同时访问同一个共享资源,如果没有互斥锁的保护,就可能出现数据竞争等问题。

然而,互斥锁的概念并不陌生,在Linux下,POSIX标准中也有互斥锁的概念,这里我们说的互斥锁是C++11语法层面提出来的概念,是C++语言自身的互斥锁std::mutex,互斥锁只有两种状态:上锁与解锁。

头文件

cpp
#include <mutex>
class mutex;

常用函数接口

cpp
// 构造函数
constexpr mutex() noexcept;
mutex(const mutex &) = delete;

// 上锁
void lock();

// 尝试上锁
bool try_lock();

// 解锁
void unlock();

使用示例

cpp
int gCount = 0;
mutex mtx; // 初始化互斥锁

void threadFunc() {
    for (int idx = 0; idx < 1000000; ++idx) {
        mtx.lock(); // 上锁
        ++gCount;
        mtx.unlock();//解锁
    }
}

int main(int argc, char *argv[]) {
    thread th1(threadFunc);
    thread th2(threadFunc);
    
    th1.join();
    th2.join();
    
    cout << "gCount = " << gCount << endl;

    return 0;
}

对于std::mutex互斥锁而言,必须手动上锁与解锁且必须成对出现,如果上锁了,但是由于某些原因没有解锁,就会导致程序一直处于锁定状态而无法解锁,所以C++11中使用的C++之父提出来的思想RAII设计了两种锁std::lock_guardstd::unique_lock,下面就来看看这两种锁的使用

lock_guard

头文件

cpp
#include <mutex>

template <class Mutex>
class lock_guard;

构造函数

cpp
explicit lock_guard(mutex_type &m);
lock_guard(mutex_type &m, std::adopt_lock_t t);
lock_guard(const lock_guard &) = delete;

特殊说明

该类没有提供其他的成员函数,不能像std::mutex一样,进行手动上锁与解锁。

使用示例

cpp
int gCount = 0;
mutex mtx; // 初始化互斥锁

void threadFunc() {
    for (int idx = 0; idx < 1000000; ++idx) {
        lock_guard<mutex> autoLock(mtx);
        ++gCount;
    }
}

int main(int argc, char *argv[]) {
    thread th1(threadFunc);
    thread th2(threadFunc);

    th1.join();
    th2.join();
    
    cout << "gCount = " << gCount << endl;

    return 0;
}

unique_lock

利用了RAII思想,相比较lock_guard而言,更加灵活,可以手动的加锁与解锁,可以配合条件变量进行使用。但是耗费资源。

头文件

cpp
#include <mutex>

template <class Mutex>
class unique_lock;

构造函数

cpp
unique_lock() noexcept;                                   //(1)
unique_lock(unique_lock &&other) noexcept;                //(2)
explicit unique_lock(mutex_type &m);                      //(3)
unique_lock(mutex_type &m, std::defer_lock_t t) noexcept; //(4)
unique_lock(mutex_type &m, std::try_to_lock_t t);         //(5)
unique_lock(mutex_type &m, std::adopt_lock_t t);          //(6)

template <class Rep, class Period>
unique_lock(mutex_type &m, const std::chrono::duration<Rep, Period> &timeout_duration); //(7)

template <class Clock, class Duration>
unique_lock(mutex_type &m, const std::chrono::time_point<Clock, Duration> &timeout_time); //(8)

常用操作

cpp
// 上锁
void lock();

// 尝试上锁
bool try_lock();

// 解锁
void unlock();

// 与时间相关锁
template <classRep, classPeriod>
booltry_lock_for(conststd::chrono::duration<Rep, Period> &timeout_duration);
template <classClock, classDuration>
booltry_lock_until(conststd::chrono::time_point<Clock, Duration> &timeout_time);

// 返回关联锁的指针
mutex_type *mutex() const noexcept;

// 释放锁
mutex_type release() noexcept;

// 锁的占有
bool owns_lock() const noexcept;

解锁示例

cpp
mutex mtx;
int gCount = 0;

void use_unique() {
    // lock可自动解锁,也可手动解锁
    unique_lock<mutex> ulk(mtx);
    gCount++;

    ulk.unlock();
    // 这句话不写,那么就在离开作用域的时候自动解锁
}

释放锁说明

返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。严格区分unlock()与release()的区别,不要混淆。如果原来mutex对像处于加锁状态,你有责任接管过来并负责解锁。(release返回的是原始mutex的指针)。

cpp
void test() {
    unique_lock<mutex> ulk(my_mutex);
    mutex *ptx = ulk.release(); // 现在你有责任自己解锁了
    
    //....

    ptx->unlock(); // 自己负责mutex的unlock了
}

检查锁是否被占用

cpp
int gCount = 0; // 可判断是否占有锁
void test() {
    // lock可自动解锁,也可手动解锁
    unique_lock<mutex> ulk(mtx);
    ++gCount++;
    if (ulk.owns_lock()) // 此时返回true,锁被占用
    {
        cout << "owns lock" << endl;
    } else {
        cout << "Doesn't own lock" << endl;
    }

    ulk.unlock();
    if (ulk.owns_lock()) // 因为刚刚已经解锁了,先占不占有锁,所以在此处返回false
    {
        cout << "owns lock" << endl;
    } else {
        cout << "Doesn't own lock" << endl;
    }
}

针对构造函数的三种锁

延迟加锁

这个参数表示暂时先不lock,之后手动去lock,但是使用之前也是不允许去lock。一般用来搭配unique_lock的成员函数去使用。当使用了defer_lock参数时,在创建了unique_lock的对象时就不会自动加锁,那么就需要借助lock这个成员函数来进行手动加锁,当然也有unlock来手动解锁。

cpp
// 可以延迟加锁
void deferLock() {
    // 延迟加锁,在构造函数中不加锁,延迟到自己手动加锁,不使用defer_lock,就会导致在构造函数加锁了
    unique_lock<mutex> ulk(mtx, std::defer_lock); // 可以加锁
    lock.lock();
    // 可以自动析构解锁,也可以手动解锁
    lock.unlock();
}

领养锁

unique_lock可以加 std::adopt_lock 参数,表示互斥量已经被lock,不需要再重复lock。该互斥量之前必须已经lock,提前加锁,才可以使用该参数。

cpp
mutex mtx;

// 同样支持领养操作
void useAdopt() {
    mtx.lock(); // 这里要加锁,使用adopt_lock之前需要先进行lock,否则会执行abort终止程序
    
    unique_lock<mutex> ulk(mtx, std::adopt_lock); // 这里是领养锁
    if (ulk.owns_lock()) // 如果上面的mtx不加锁,此处还是没有问题,还是true,但是后面不能解锁
    {
        cout << "owns lock" << endl;
    } else {
        cout << "does not have the lock" << endl;
    }
    ulk.unlock(); // 此处可以不写,lock在构造的时候,mtx已经上锁,此处可以借助lock析构解锁
}

尝试锁

可以避免一些不必要的等待,会判断当前mutex能否被lock,如果不能被lock,可以先去执行其他代码。这个和adopt不同,不需要自己提前加锁。举个例子来说就是如果有一个线程被lock,而且执行时间很长,那么另一个线程一般会被阻塞在那里,反而会造成时间的浪费。那么使用了try_to_lock后,如果被锁住了,它不会在那里阻塞等待,它可以先去执行其他没有被锁的代码。

cpp
#include <iostream>
#include <mutex>
#include <thread>

using std::cout;
using std::endl;
using std::mutex;
using std::thread;
using std::unique_lock;

mutex mtx;

void threadFunc1(int &value) {
    for (int idx = 1; idx <= 5000; idx++) {
        unique_lock<mutex> ulk(mtx, std::try_to_lock);
        if (ulk.owns_lock() == true) {
            value += idx;
        } else {
            // 执行一些没有共享内存的代码
        }
    }
}

void threadFunc2(int &value) {
    for (int idx = 5001; idx <= 10000; idx++) {
        unique_lock<std::mutex> ulk(mtx, std::try_to_lock);
        if (ulk.owns_lock() == true) {
            value += idx;
        } else {
            // 执行一些没有共享内存的代码
        }
    }
}

int main() {
    int num = 0;
    thread t1(threadFunc1, std::ref(num));
    thread t2(threadFunc2, std::ref(num));

    t1.join();
    t2.join();

    cout << "num = " << num << endl; // 不一定得到50005000

    return 0;
}