• C++线程安全队列


    在异步编程中,经常需要一个多线程安全的队列来作为线程间通讯的结构,但STL本身提供的std::queue并不是线程安全的,所以需要自己手动实现。

    1. #pragma once
    2. #include
    3. #include
    4. #include
    5. template <typename T>
    6. class SafeQueue {
    7. public:
    8. using lock_type = std::unique_lock;
    9. public:
    10. SafeQueue() = default;
    11. ~SafeQueue() = default;
    12. template<typename IT>
    13. void push(IT &&item) {
    14. static_assert(std::is_same_vdecay_t>, "Item type is not convertible!!!");
    15. {
    16. lock_type lock{mutex_};
    17. queue_.emplace(std::forward(item));
    18. }
    19. cv_.notify_one();
    20. }
    21. auto pop() -> T {
    22. lock_type lock{mutex_};
    23. cv_.wait(lock, [&]() { return !queue_.empty(); });
    24. auto front = std::move(queue_.front());
    25. queue_.pop_front();
    26. return front;
    27. }
    28. private:
    29. std::queue queue_;
    30. std::mutex mutex_;
    31. std::condition_variable cv_;
    32. };

    需要注意的点:

    1. 利用模板类来实现,以适配不同的元素类型。

    2. 典型的生产者-消费者模型,需要用到锁和条件变量,这里简单说明一下:

    锁:

    C++11中,在语言层面引入了同步机制,其中std::mutex和std::unique_lock都是这个时候引入的。在Linux平台,它们是对pthread_mutex_t pthread_mutex_lock的封装。

    需要注意的是std::mutex不允许复制也不允许移动,std::unique_lock不允许复制,但允许移动。

    条件变量:

    和锁类似,条件变量用于控制两个线程的执行顺序,主要包括wait()和notify()两类接口(有各种形式,这里用的wait和notify_one是其中一组),wait是等待某一条件满足,如果满足则继续执行,否则会进入一个waitting队列,而notify是从队列中唤醒一个线程,去执行。

    注意条件变量一定要配合锁使用,不能单独使用。

    还有一个细节是,notify_one可以在解锁后调用。

    3. 尾置返回类型

    auto pop() -> T 和 T pop()在此处的作用相同,但另外一些情况则必须使用尾置类型返回,就是当返回类型是根据传入的参数决定的时候。

    4. 通用引用

    push的实现有点儿复杂,它没有直接使用类的模板类型,而是重新定义了一种类型,这么做是为了使用通用引用达到完美转发的目的。最终实现的效果是:如果被push的是一个左值,则调用起拷贝构造,如果被push的是一个右值,则调用其移动构造。

    但这么写可能导致其类型与类的模板类型不一致,所以要对其做一个类型校验,这也是模板编程中常见到的,由于模板是一种强制类型匹配,没有了继承体系的限制,所以要程序员自己去判断。

    如果觉得这么写太牵强,也可以利用函数重载,左值引用右值引用分开处理:

    1. void push(T &item) {
    2. {
    3. lock_type lock{mutex_};
    4. queue_.emplace(item);
    5. }
    6. cv_.notify_one();
    7. }
    8. void push(T &&item) {
    9. {
    10. lock_type lock{mutex_};
    11. queue_.emplace(std::move(item));
    12. }
    13. cv_.notify_one();
    14. }

    这里可以再引入一个问题:为什么通用引用只能发生在类型推断时?

    假如没有类型推断,我们可以使用通用引用吗?通用引用的语义是:在需要复制对象的时候,如果是对象是左值,调用拷贝构造,如果对象是右值,调用移动构造。这种如果是什么什么类型就怎么怎么样的语义,叫作多态。多态必须由特定的语法来实现。说白了,如果没有模板,我们直接用一种符号(比如&&&)来表示通用引用,那再编译这函数的时候,是按照左值编译还是按照右值编译?是调用拷贝构造函数调用移动构造?这是个无解的问题。所以这种多态语义要么自己用函数重载去挨个实现,要么利用模板推导来实现。所以通用引用必须和模板类型推导绑定。

  • 相关阅读:
    第三部分—数据结构与算法基础_2. 线性表
    Socks5代理和代理IP:网络工程师的多面利器
    CPT205-Computer Graphics
    gpt-4o考场安排
    顺丰面试,第二个问题把我劝退了!
    Netty 高性能原因之一 采用了高性能的NIO 模式
    8086与8088
    Kafka生产经验
    Sentinel技术学习与总结待续
    linux生产者消费者模型
  • 原文地址:https://blog.csdn.net/cyfcsd/article/details/130839950