C++多线程锁的基本用法

线程同步的基本概念

线程同步不是一起、相同,而是协调、协同的意思。

  1. 按预定的先后次序进行运行,线程A生成数据后交给线程B处理;

  2. 公共资源同一时刻只能被一个线程使用;共享数据在同一时刻只能被一个线程修改,以保证数据的完整性。

包括的内容有“互斥锁、条件变量、信号量、自旋锁、读写锁

一、互斥锁

头文件#include

std::mutex:

声明:mutex mtx;

1.对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁

2.mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生。

  1. mtx.unlock():释放锁

  2. std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。

pthread_mutex_t mutex; //声明互斥锁

pthread_mutex_init(&mutex,NULL); //初始化锁

pthread_mutex_lock(&mutex); //阻塞加锁

pthread_mutex_trylock(&mutex);//非阻塞加锁

pthread_mutex_unlock(&mutex);//解锁

pthread_mutex_destroy(&mutex); //销毁锁,释放资源

二、lock_guard

虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”(线程2:我的心在等待永远在等待……),此时就发生了死锁。给一个发生死锁的 :

死锁的情况:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        mtx.lock();
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        mtx.unlock();
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

结果

[root@2d129aac5cc5 demo]# ./mutex_demo3_dead_lock
id:1, throw excption....

程序并没有退出,而是永远的“卡”在那里了,也就是发生了死锁。

那么这种情况该怎么避免呢? 这个时候就需要std::lock_guard登场了。std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(这就是所谓的RAII,读者可自行搜索)。我们修改一下demo:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx; // 保护counter

void increase_proxy(int time, int id) {
    for (int i = 0; i < time; i++) {
        // std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
        // std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
        std::lock_guard<std::mutex> lk(mtx);
        // 线程1上锁成功后,抛出异常:未释放锁
        if (id == 1) {
            throw std::runtime_error("throw excption....");
        }
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

void increase(int time, int id) {
    try {
        increase_proxy(time, id);
    }
    catch (const std::exception& e){
        std::cout << "id:" << id << ", " << e.what() << std::endl;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 10000, 1);
    std::thread t2(increase, 10000, 2);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

结果:

[root@2d129aac5cc5 demo]# ./mutex_demo4_lock_guard
id:1, throw excption....
counter:10000

所以,推荐使用std::mutex和std::lock_guard搭配使用,避免死锁的发生

std::lock_guard的第二个构造函数

第二个构造函数有两个参数,其中第二个参数类型为:std::adopt_lock_t。这个构造函数假定:当前线程已经上锁成功(必须要把互斥量提前lock,否则会报异常)

三、unique_lock

unique_lock相比较lock_guard更灵活,但效率低,因为要维护锁的状态,内存占用更多。

3.1 unique_lock的第二个参数

1. std::adopt_lock

与lock_guard的作用一样,需要提前上锁。

2.try_to_lock

我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里;

3.defer_lock

如果没有第二个参数就对mutex进行加锁,加上defer_lock是始化了一个没有加锁的mutex;

不给它加锁的目的是以后可以调用unique_lock的一些方法;

前提:不能提前lock()。

3.2 unique_lock的成员函数

3.3 unique_lock的成员函数(前三个与defer_lock联合使用)

3.3.1 lock():加锁

unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();

3.3.2 unlock():解锁

void inMsgRecvQueue(){
    for (int i = 0; i < 10000; i++){

        cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
        std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex
        sbguard.lock();//咱们不用自己unlock
        //处理共享代码

        //因为有一些非共享代码要处理
        sbguard.unlock();
        //处理非共享代码要处理。。。

        sbguard.lock();
        //处理共享代码

        msgRecvQueue.push_back(i);
        //...
        //其他处理代码
        sbguard.unlock();//画蛇添足,但也可以
    }
}

因为一些非共享代码要处理,可以暂时先unlock(),用其他线程把它们处理了,处理完后再lock()。

3.3.3 try_lock():尝试给互斥量加锁

如果拿不到锁,返回false,否则返回true。

void inMsgRecvQueue(){
    for (int i = 0; i < 10000; i++)
    {
        std::unique_lock<std::mutex> sbguard(my_mutex, std::defer_lock);//没有加锁的my_mutex

        if (sbguard.try_lock() == true)//返回true表示拿到锁了
        {
            msgRecvQueue.push_back(i);
            //...
            //其他处理代码
        }
        else
        {
            //没拿到锁
            cout << "inMsgRecvQueue()执行,但没拿到锁头,只能干点别的事" << i << endl;
        }
    }
}

lock的代码段越少,执行越快,整个程序的运行效率越高。

a.锁住的代码少,叫做粒度细,执行效率高;

b.锁住的代码多,叫做粒度粗,执行效率低;

四、复制与转移

lock_guard不能复制也不能转移,unique_lock可以转移,不可以复制。

// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1;  // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok

// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1;  // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error

原文链接: https://www.cnblogs.com/cuijy1/p/16225100.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/189016

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月12日 下午2:35
下一篇 2023年2月12日 下午2:36

相关推荐