一文看懂C++多线程编程之互斥锁、条件变量

目录

一、互斥锁mutex的用法

二、lock_guard和unique_lock的用法

三、生产消费模型之条件变量

四、线程池、信号量、死锁问题


项目中经常出现多个线程对共享资源进行操作的情况,如果不同线程对同一资源的操作导致数据前后不一致就会出现问题!因此多线程编程需要考虑线程安全的问题。

一、互斥锁mutex的用法

C++当中用到的一个类是mutex,这个中文就是互斥量的意思,顾名思义,就是一个时刻只能有一个访问。

通俗来讲,在一个线程里处理完我们所需要的数据之后,然后才将控制权交出,这个就是用到这个东西。

互斥锁的使用非常简单:

  1. 包含头文件#include<mutex>
  2. 实例化对象:mutex mt;
  3. 在需要加锁的地方,调用metex的lock()方法,解锁的地方调用unlock()方法。

示例如下:

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
 
mutex mt;
void thread_task()
{
    for (int i = 0; i < 10; i++)
    {
        mt.lock();
        cout << "print thread: " << i << endl;
        mt.unlock();
    }
}
 
int main()
{
    thread t(thread_task);
    for (int i = 0; i > -10; i--)
    {
        mt.lock();
        cout << "print main: " << i << endl;
        mt.unlock();
    }
    t.join();
    return 0;
}

二、lock_guard和unique_lock的用法

上一章介绍了mutex加锁解锁的方法,为了防止使用mutex加锁解锁的时候,忘记解锁unlock了,后续出现了lock_guard和unique_lock两种方法。

  1. lock_guard

其用法也很简单:

(1)包含头文件#include<mutex>

(2)实例化对象:mutex mt;

(3)在需要加锁的地方:lock_guard<mutex> guard(mt);

这个方法是在lock_guard构造函数里加锁,在析构函数里解锁。构造函数加锁即定义的地方加锁,在作用域结束{ }的时候自动调用析构函数解锁。

存在的缺点:如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。

#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
 
mutex mt;
void thread_task()
{
    for (int i = 0; i < 10; i++)
    {
        lock_guard<mutex> guard(mt);
        cout << "print thread: " << i << endl;
    }
}
  1. unique_lock

用法:unique_lock<mutex> unique(mt);

这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太大的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。

优点:增加了中途解锁的功能,可以缩小锁的粒度范围

缺点:unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。

三、生产消费模型之条件变量

上文提到的”加锁“其实就是对资源具有独占执行的权限,其他抢夺资源的线程需要等待。

对于生产者-消费者模型来说,生产者生产数据到一个队列,消费者读取数据,如果生产者生产过慢而消费者读取过快,导致队列数据为空,那么就会产生两个问题:

1、如果消费者还去读,就会抛出异常;

2、如果消费者先判断是否为空在考虑读不读,方法可以,但是需要不断的循环判断这个条件,会浪费cpu的资源。

因此条件变量会解决这个问题:在如果在队列没有数据的时候,消费者线程能一直阻塞在那里,等待着别人给它唤醒,在生产者往队列中放入数据的时候通知一下这个等待线程,唤醒它,告诉它可以来取数据了。

使用方法:

1、包含头文件:#include <condition_variable>

2、wait()可以让线程陷入休眠状态,意思就是不干活了

3、notify_one()就是唤醒真正休眠状态的线程,开始干活了

4、notify_all()这个接口,顾名思义,就是通知所有正在等待的线程,起来干活了。

以下示例:

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
 
deque<int> q;
mutex mt;
condition_variable cond;
 
void thread_producer()
{
    int count = 10;
    while (count > 0)
    {
        unique_lock<mutex> unique(mt);
        q.push_front(count);
        unique.unlock();
        cout << "producer a value: " << count << endl;
        cond.notify_one();
        this_thread::sleep_for(chrono::seconds(1));
        count--;
    }
}
 
void thread_consumer()
{
    int data = 0;
    while (data != 1)
    {
        unique_lock<mutex> unique(mt);
        while (q.empty())
            cond.wait(unique);
        data = q.back();
        q.pop_back();
        cout << "consumer a value: " << data << endl;
        unique.unlock();
    }
}
 
int main()
{
    thread t1(thread_consumer);
    thread t2(thread_producer);
    t1.join();
    t2.join();
    return 0;
}

生产者:首先生产者利用unique_lock来加锁,然后将生产的数据放入队列,打印,解锁,一旦解锁之后,消费者获得了执行机会。

消费者:另一方面消费者就会通过unique_lock获得控制权,也就是获得锁,然后判断队列为空的话就一直盗用wait()函数阻塞在那里,等待其他线程来唤醒它。而阻塞该线程时,该函数会自动解锁,允许其他线程执行。

生产者:再次回到生产者这里,生产者线程利用利用条件变量cond.notify_one()来通知阻塞的线程起来干活了。

消费者:阻塞在那里的消费者线程一旦得到notify唤醒,该函数取消阻塞并获取锁,然后取出队列中的数据,并打印,最后解锁。

生产者:再次回到生产者,然后生产者休眠1秒,这里休眠是为了模拟生产者生产慢的情况,实际开发的时候不要去休眠。最后减一,进入下一次生产。

四、线程池、信号量、死锁问题

当前项目还没有碰到,碰到后继续学习更新。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值