多线程资源共享


c++线程资源共享

我们知道,在多线程中,如果两个线程都使用到了同一份资源,那么这两个线程对资源修改和使用的先后顺序是不确定的。

比如有一副1024*1280的图像,一个线程需要不断地更新图像,另一个线程需要使用这个图像并且将其显示出来。如果修改图像的线程运行到一半的时候,显示图像的线程就将数据提取出来显示,那么就可能产生图像残缺的现象。这会对我们的应用产生不可预测的结果。这种线程相互争夺资源的现象,被称为线程不安全。

要想安全地使用资源,那么就必须设计一定的规则,使得到资源的线程处理;没有得到资源的线程休眠,以此达到线程安全和充分利用cpu的目的。


使用互斥锁mutex

c++ mutex使用手册

基础用法:

    std::mutex your_mutex; //定义你的锁

    your_mutex.lock();   //上锁

    //使用你的资源

    your_mutex.unlock(); 

注意同一份资源应该使用同一个锁

当一个线程调用这个锁对象的lock()函数之后,如果第二个线程也调用了lock函数,那么第二个线程就会”被迫等待”,类似于进入一个while循环,直到第一个线程调用unlock()函数。

使用互斥锁之后,就能保证每一个时刻都最多只有一个线程在处理这份资源。

mutex的缺点:等待资源的线程会频繁唤醒cpu,导致程序的性能下降。


条件变量+智能锁

c++ 条件变量使用手册

c++ unique_lock使用手册

使用互斥锁可能会造成死锁的情况,即上锁之后忘记解锁,导致资源一直不能释放,等待资源的线程卡死。

使用互斥锁导致频繁唤醒cpu会导致性能消耗极大。

使用条件变量和智能锁能一定程度上解决这些问题

使用智能锁unique_lock能够在这把锁的声明周期过后自动释放,故避免了死锁的问题

使用条件变量,当生产者生产完之后,给消费者一个信号,消费者才可以进行处理,否则的话,消费者会一直休眠。使用这种方法,降低了唤醒cpu的频率。导致程序的性能提高。

有了条件变量还需要使用锁的原因是条件变量可能会误唤醒,加锁是第二层保障

基本使用(生产者)

    std::condition_variable cond;  //cond为线程之间共享的变量
    {
        std::unique_lock<std::mutex> my_lock;

        //生产资源

        cond.notify_one();  //唤醒等待的线程
    } //过了unique_lock的生命周期之后,锁自动释放
    std::condition_variable cond;  //cond为线程之间共享的变量
    {
        std::unique_lock<std::mutex> my_lock;

        cond.wait(); //等待资源

        //消费资源
    } //过了unique_lock的生命周期之后,锁自动释放

条件变量和智能锁一定程度上提高了程序的性能,但由于还带锁,锁造成的低性能仍然没有发生本质上的改变。如果要使程序处于无锁的良好环境,减少数据竞争的话,需要使用到原子变量的无锁编程。


无锁循环队列

c++ atomic使用手册

要很好地理解以下的代码,就先要了解原子变量内存模型。某个语句f中的一个变量a在下面代码有依赖的意思是下面某句代码g直接或者间接用到了变量a,故编译器会自动将g代码语句放在f后面。否则的话编译器可以自动安排语句的执行顺序,来提高代码的运行效率。

以下代码适用于生产者-消费者模型

#ifndef _LOCKFREE_QUEUE_
#define _LOCKFREE_QUEUE_
#include <iostream>
#include <atomic>   //原子标准库
#define FIXED_NUM 16 //设置为2的整数次方 方便处理,因为对2的整数次方作除法可以用左移表示
    template <typename T> //模板类,T为你需要共享的数据类型
    class LockFreeQueue
    {
        struct Element
        {
            T data_;    //数据存储区
            std::atomic<bool> is_full; //是否有数据
        };

    public:
        LockFreeQueue();
        ~LockFreeQueue();
        bool push(T &input);  //入栈
        bool pop(T &output);  //出栈
        std::atomic<short> write_index;  //写索引
        Element data_queue[FIXED_NUM];  //固定大小的数组,通过write_index构成循环队列的效果
    };
    template <typename T>
    LockFreeQueue<T>::LockFreeQueue()
    {
        write_index = 0;  //初始化为0
    }
    template <typename T>
    LockFreeQueue<T>::~LockFreeQueue()
    {
    }
    template <typename T>
    bool LockFreeQueue<T>::push(T &input)
    {
        //由于后面的语句都对write_index_temp有依赖,故此处的内存模型可以忽略,或者说用什么都行
        short write_index_temp = write_index.load(std::memory_order_relaxed);
        data_queue[write_index_temp].data_ = std::move(input); //利用std::move可以加快处理速度,移动过后的对象input不再可用
        //memory_order_release内存模型强制前一条语句在这一条之前运行,即必须要修改完data_再将is_full置true
        data_queue[write_index_temp].is_full.store(true, std::memory_order::memory_order_release);
        write_index_temp++; 
        write_index_temp %= FIXED_NUM; //构成循环

        //强制使write_index_temp修改后再运行,但其实由于write_index_temp用到了write_index_temp,故即使不用memory_order_release模型,也会在write_index_temp自增之后运行
        write_index.store(write_index_temp, std::memory_order::memory_order_release);
        return true;
    }
    template <typename T>
    bool LockFreeQueue<T>::pop(T &output)
    {
        //索引到读取index的前一个,更好地达到同步的效果
        short read_index = write_index.load(std::memory_order::memory_order_relaxed);
        read_index = (read_index + FIXED_NUM - 1) % FIXED_NUM;
        //强制后面的语句在这一条语句之后运行,即必须要is_full为true才执行后面的代码
        if (data_queue[read_index].is_full.load(std::memory_order_acquire))
        {
            output = std::move(data_queue[read_index].data_);
            //强制前面的代码在这句代码之前执行
            data_queue[read_index].is_full.store(false, std::memory_order::memory_order_release);
            return true;
        }
        return false;
    }
#endif

使用以上代码,当生产资源时,使用push函数;当消费资源时,使用pop函数


文章作者: 闲梦溪
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 闲梦溪 !