C++并发编程下栈和队列的线程安全代码 及 注释

文章讨论了如何在多线程环境中实现线程安全的stack和queue。stack的实现中,通过抛出异常避免条件竞争,使用mutex保护top和pop操作。queue的实现则利用互斥量和条件变量,提供wait_and_pop和try_pop等方法,确保线程安全。
摘要由CSDN通过智能技术生成

写的很清晰了 stack和queue直接看对应的注释应该能看懂 

stack的线程安全代码

削减的接口有一部分为了解决接口冲突

消失的拷贝赋值函数是为了解决数据冲突

#include <exception>
#include <memory>
#include <mutex>
#include <stack>
//相较于标准stack,削减了很多接口
//empty size 在多线程的环境下并不可靠
//因为返回后,其他的线程可以自由的访问栈,push pop 之前的empty和size就有问题
//如果是单线程的情况下,先判断empty再访问top是可行的
//但是 在共享的栈对象里,这样调用会出问题 因为在empty和top这段时间里 随时都会有pop进来
//这就是著名的条件竞争了
//so solution?
//问题来自于接口设计,所以解决也得在接口设计上解决

//当发现栈已经是空的了,那就抛出异常   <-stupid  因为这样如果返回值empty是false,你也需要异常捕获,会让empty多余
//再分析一下 还有什么潜在的条件竞争条件?
//调用top和pop之间,当两个线程运行前面代码,引用同一个栈对象s,那么很容易就会出现pop时间不一致的情况,从而导致value是旧值
//数据库的事务里面的话,这个叫脏读吧可能,读到了本应该已经消失的数据
//所以接口设计就要出现大的改动了
//使用同一个互斥量来保护 top和pop
//question? 如果一个对象的拷贝函数在栈中抛出一个异常,这样的处理方式会出问题
//想想vector,在高负荷的情况下进行拷贝,就会导致内存分配失败。所以会抛出一个异常
//pop的时候会出什么问题呢? 如果这个值返回调用函数的时候,栈才会改变,但拷贝数据的时候,调用函数抛出异常会怎么样?事情真的发生了,
//弹出的数据会丢失,但拷贝失败
//所以 这就是为什么std::stack里,设计人员把这个操作分为两部分
//先获得顶部 的数据,top  再pop 这样的话,不能安全的将元素拷贝,数据仍旧存在 <-我觉得这个也算是STL的强异常安全性的体现
//但是,我们这里为什么不能分开呢? 因为 分开了,就会造成条件竞争
//我们只能把他们放在一起,那么如何解决这些现象呢?
//传入一个引用  将变量的引用作为参数,传入pop函数获得想要的弹出值
// pop(res); 这样的话
//需要在pop里构造栈类型的实例,用来接收目标值  对于一些类型,这样做不现实,临时构造一个实例,从时间资源不划算  甚至有的自定义的类型没有移动拷贝构造
//为什么要有拷贝构造?  返回一个值的时候,有移动就移动,没移动就只能拷贝了

//无异常抛出的拷贝构造和移动构造  忽视了那些没有移动构造的类型,不能接受
//返回指向弹出值的指针
//很好的避免了资源不够抛出异常的问题,但指针需要对内存分配管理,所以最好使用shared_ptr 避免内存泄漏,也能交给标准库控制内存分配,不需要new delete



//综上,解释这里的设计
//栈为什么不能直接赋值呢?
//因为拷贝赋值的情况下,会出现多线程中 多个线程对同一个对象修改,从而影响了线程的安全性
//为了保证线程安全,通常禁止声明拷贝赋值函数
//移动构造和移动赋值一般不会有影响
//所以就剩了push pop empty 三个函数
struct empty_stack : std::exception
{
    const char *what() const throw()
    {
        return "empty stack!";
    };
};
template <typename T>
class threadsafe_stack
{
private:
    std::stack<T> data;
    mutable std::mutex m;

public:
    threadsafe_stack()
        : data(std::stack<T>()) {}
    threadsafe_stack(const threadsafe_stack &other)
    {
        std::lock_guard<std::mutex> lock(other.m);
        data = other.data; // 1 在构造函数体中的执行拷贝
    }
    threadsafe_stack &operator=(const threadsafe_stack &) = delete;
    //加锁
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(new_value);
    }
    //返回值了 加锁 返回指针 shared_ptr 作为保护
    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty())
            throw empty_stack(); // 在调用pop前,检查栈是否为空
        std::shared_ptr<T> const res(std::make_shared<T>(data.top())); // 在修改堆栈前,分配出返回值
        data.pop();
        return res;
    }
    //这个传入引用,会进行一个栈类型的复制,得确保T有移动赋值函数/拷贝赋值函数
    void pop(T &value)
    {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty())
            throw empty_stack();
        value = data.top();
        data.pop();
    }
    //加错 查empty
    bool empty() const
    {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
    }
};

queue的线程安全实现方法 

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
//queue的队列 
//对整个队列的状态查询 empty size
//查队列的元素 front back
//修改队列的操作 push pop emplace
//固有接口也会有条件竞争   所以 front和pop 合在一起使用 
//队列里传递数据 接收线程需要等待数据的压入  pop的两个变种 try_pop 和 wait and pop 
//try pop 尝试从队列里弹出数据 总会直接返回,无论成功失败
//wait pop 等到有值的时候才会返回

//那么 队列的实例里 包含互斥量和条件变量 
//独立的变量 data_queue 就不需要了
//调用push也不需要同步,
//wait的话 还需要兼顾条件变量的等待
template <typename T>
class threadsafe_queue
{
private:
    mutable std::mutex mut; // 1 互斥量必须是可变的
    std::queue<T> data_queue;
    std::condition_variable data_cond;

public:
    threadsafe_queue()
    {
    }
    threadsafe_queue(threadsafe_queue const &other)
    {
        std::lock_guard<std::mutex> lk(other.mut);
        data_queue = other.data_queue;
    }
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(new_value);
        data_cond.notify_one();
    }
    //等到了 这个mut  
    void wait_and_pop(T &value)
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]
                       { return !data_queue.empty(); });
        value = data_queue.front();
        data_queue.pop();
    }
    //等待这个mut
    std::shared_ptr<T> wait_and_pop()
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]
                       { return !data_queue.empty(); });
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }
    //这个直接pop
    bool try_pop(T &value)
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return false;
        value = data_queue.front();
        data_queue.pop();
        return true;
    }
    //无需wait,没有就返回个空的玩意
    std::shared_ptr<T> try_pop()
    {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty())
            return std::shared_ptr<T>();
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }
    //成员函数 const 传入拷贝构造函数的other形参是const引用      
    //因为其他线程可能有这个类型的非const引用对象 并调用变种成员函数 所以这里需要对互斥量上锁
    //锁住的互斥量是一个可变的操作,所以互斥量需要时mutable 才能在empty和拷贝构造里锁住
    bool empty() const
    {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

### 回答1: "并发编程实战"是一本经典的编程书籍,该书的指导了我们如何处理并发编程中的各种问题。在书中,作者通过详细的示例代码向读者展示了如何解决并发编程中的各种挑战。 书中的示例代码是作者通过实践总结出来的,能够真实地反映出并发编程的现实情况。它们涵盖了并发编程的各个方面,包括线程间的协作、共享资源的同步访问、死锁的避免和解决等等。 这些示例代码不仅仅是为了演示问题,更重要的是教会读者如何正确地处理并发编程中的问题。通过学习这些示例代码,我们可以了解到并发编程中常见的错误和陷阱,并学会如何规避它们。 在书中,作者并没有止步于示例代码的展示,他还详细地解释了每段代码的设计思路和原理。通过深入地解析这些示例代码,我们可以更好地理解并发编程中的关键概念和技术,从而能够在实际项目中灵活运用。 总的来说,"并发编程实战"这本书的示例代码是非常实用和有价值的。通过学习和理解这些代码,我们可以提升自己在并发编程领域的能力,更好地应对并发编程中的各种挑战。 ### 回答2: "C 并发编程实战" 是一本经典的编程书籍,详细介绍了并发编程的各个方面包括并发模型、线程同步、线程安全、锁机制、线程池等。这本书的核心在于通过源代码实例,帮助读者深入理解并发编程概念和技术,并从实践角度掌握相关知识。 "C 并发编程实战"源代码示例给读者提供了一个实践的框架,使他们能够在实际的项目中应用并发编程技术。这些源代码示例往往是基于实际应用场景设计的,通过实例代码的编写和运行,读者可以更加直观地感受到并发编程的特点和挑战。 源代码示例可能涉及到多线程的创建、线程同步的实现、线程安全的问题处理等。例如,可以有一个生产者-消费者模型的示例,展示如何通过多线程实现任务的生产和处理,以及如何使用锁机制来保证线程安全。另外,还可以有一个基于线程池的示例,用于展示如何利用线程池提高并发性能,以及如何管理线程池的各个参数。 通过源代码示例,读者可以学习到在并发编程中的方方面面的知识,掌握并发编程的基本技能。同时,通过实践中的错误和问题,读者还可以学会如何处理常见的并发编程问题,如死锁、饥饿等。 总之,"C 并发编程实战"的源代码示例是一种非常宝贵的学习资源,它能够帮助读者深入理解并发编程的概念和技术,并通过实践掌握相关的编程技能。 ### 回答3: "c并发编程实战"是一本关于并发编程的经典书籍,旨在帮助读者理解和应用并发编程的原理和技术。在该书中,作者通过详细讲解并发编程的核心概念,并提供了丰富的源代码示例,以帮助读者更好地理解和运用这些概念。 书中的源代码示例非常实用,涵盖了多线程、锁、线程池、阻塞队列、并发容器等多个方面的内容。这些示例代码通过具体的场景和问题,展示了不同并发编程技术的应用,使读者能够学以致用。 源代码示例中,作者注重讲解每个示例的实现原理和核心思想,并配以详细的注释,方便读者理解。读者可以通过运行这些示例代码,深入理解并发编程的实际应用,并通过代码的演示,发现其中的问题和解决方案。 此外,书中的源代码示例还提供了一些常见的并发编程模式,如生产者-消费者模式、读写锁模式等。这些模式可以帮助读者在实际项目中更好地设计和开发高效的并发程序。 总的来说,“c并发编程实战”这本书的源代码示例丰富实用,帮助读者深入理解并发编程的原理和技术,并能够应用这些知识解决实际问题。无论是新手还是有一定经验的开发者,都可以从中获益匪浅。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值