在异步编程中,经常需要一个多线程安全的队列来作为线程间通讯的结构,但STL本身提供的std::queue并不是线程安全的,所以需要自己手动实现。
- #pragma once
-
- #include
- #include
- #include
-
- template <typename T>
- class SafeQueue {
- public:
- using lock_type = std::unique_lock
; -
- public:
- SafeQueue() = default;
-
- ~SafeQueue() = default;
-
- template<typename IT>
- void push(IT &&item) {
- static_assert(std::is_same_v
decay_t>, "Item type is not convertible!!!"); - {
- lock_type lock{mutex_};
- queue_.emplace(std::forward
(item)); - }
- cv_.notify_one();
- }
-
- auto pop() -> T {
- lock_type lock{mutex_};
- cv_.wait(lock, [&]() { return !queue_.empty(); });
- auto front = std::move(queue_.front());
- queue_.pop_front();
- return front;
- }
-
- private:
- std::queue
queue_; - std::mutex mutex_;
- std::condition_variable cv_;
- };
需要注意的点:
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的是一个右值,则调用其移动构造。
但这么写可能导致其类型与类的模板类型不一致,所以要对其做一个类型校验,这也是模板编程中常见到的,由于模板是一种强制类型匹配,没有了继承体系的限制,所以要程序员自己去判断。
如果觉得这么写太牵强,也可以利用函数重载,左值引用右值引用分开处理:
-
- void push(T &item) {
- {
- lock_type lock{mutex_};
- queue_.emplace(item);
- }
- cv_.notify_one();
- }
-
- void push(T &&item) {
- {
- lock_type lock{mutex_};
- queue_.emplace(std::move(item));
- }
- cv_.notify_one();
- }
这里可以再引入一个问题:为什么通用引用只能发生在类型推断时?
假如没有类型推断,我们可以使用通用引用吗?通用引用的语义是:在需要复制对象的时候,如果是对象是左值,调用拷贝构造,如果对象是右值,调用移动构造。这种如果是什么什么类型就怎么怎么样的语义,叫作多态。多态必须由特定的语法来实现。说白了,如果没有模板,我们直接用一种符号(比如&&&)来表示通用引用,那再编译这函数的时候,是按照左值编译还是按照右值编译?是调用拷贝构造函数调用移动构造?这是个无解的问题。所以这种多态语义要么自己用函数重载去挨个实现,要么利用模板推导来实现。所以通用引用必须和模板类型推导绑定。