前面设计同步队列的代码,下面详细说说实现。
Push
void Push(T&& task) {
std::unique_lock<std::mutex> lock{mutex_};
not_full_variable_.wait(lock, [this]{ return queue_.size() < max_size_; });
queue_.push(std::forward<T>(task));
not_empty_variable_.notify_one();
}
void Push(const T& task) {
std::unique_lock<std::mutex> lock{mutex_};
not_full_variable_.wait(lock, [this]{ return queue_.size() < max_size_; });
queue_.push(std::move(task));
not_empty_variable_.notify_one();
}
为了保证线程安全,首先创建了一个unique_lock获取mutex,接着通过队列非满条件变量来等待队列非满的时候。这里使用lambda表达式来返回队列是否已满,如果条件不满足,条件变量会释放mutex,并将线程置于waiting状态,等待其他线程发出通知唤醒自己;如果条件满足,则继续往下执行,往队列中添加任务,接着发出队列非空的通知,唤醒一个正在处于等待状态的线程取走任务。
右值引用
这里有两个Push函数,其实它们实现的功能是一样的,参数为T&&task是为了实现移动语句,它其实是一个不定的类型,可能是左值也可能是右值,如果参数是右值,就可以避免对临时对象的深拷贝,提高性能。
std::forward
一个右值引用参数作为函数的形参时,在这个函数内部如果再传递该参数给其函数,这个参数已经变成了一个左值,不会按照我们预期的类型进行传递。std::forward就是为了实现需要的完美转发,即保持参数的类型。不管参数T&&这种未定的引用还是明确的左值引用或者右值引用,它会按照参数本来的类型转发。
std::move
可以将一个左值强制转换为一个右值引用,这样就可以通过移动构造,避免深拷贝。
Pop
void Pop(T& task) {
std::unique_lock<std::mutex> lock{mutex_};
not_empty_variable_.wait(lock, [this]{ return !queue_.empty(); });
task = queue_.front();
queue_.pop();
not_full_variable_.notify_one();
}
void Pop(std::queue<T>& tasks) {
std::unique_lock<std::mutex> lock{mutex_};
not_empty_variable_.wait(lock, [this]{ return !queue_.empty(); });
tasks = std::move(queue_);
not_full_variable_.notify_one();
}
这里有两个Pop函数,功能分别为只获取一个任务,和获取全部任务。如果只有获取一个任务接口,因为每次获取任务都需要获取锁,想要获取全部任务就需要调用多次,效率比较低。因此设计多设计了一个获取全部任务的接口,通过move的方式,将队列的所有任务都交换出来,避免了数据的复制。
它的流程和Push的过程类似,先获取mutex,然后通过队列非空条件变量来等待队列非空的时候,不满足时释放mutex继续等待,满足则会将任务从队列中取出,并唤醒一个在等待添加任务的线程去添加任务。
Empty Full Count Clear
Empty函数用于判断队列是否为空
Full函数用于判断队列是否已满
Count函数用于获取队列中的等待执行的任务数量
Clear函数用于清除队列中等待的任务
锁和条件变量
std::mutex
独占互斥锁,可以通过lock()方法阻塞线程,直到使用unlock()来解除对互斥量的占用。
std::lock_guard
只使用std::mutex需要先调用lock()方法锁,再调用unlock()来释放锁。因此,有时候就可能会出现忘记调用unlcok()的情况。而调用std::lock_guard可以简化锁的使用,它在构造的时候会自动锁定互斥锁,在作用域结束的时候自动地解锁。
std::condition_variable
条件变量需要和互斥量配合使用,它可以阻塞一个或者多个线程,直到收到其他线程发出的通知或者超时才会唤醒当前阻塞的线程。
std::unique_lock<std::mutex> lock{mutex_};
not_empty_variable_.wait(lock, [this]{ return !queue_.empty(); });
unique_lock和std::lock_guard是类似的,可以保证安全释放mutex,区别在于std::lock_guard需要等到作用域结束才释放mutex,而unique_lock可以自由的释放mutex。因此可以和条件变量配置使用。
not_empty_variable_就是一个std::condition_variable,它会先检查队列非空条件是否满足,如果满足则重新获取mutex,结束wait继续执行;如果不满足则会释放mutex,将线程置为waiting状态,等待唤醒。
现在我们已经实现了同步队列,下面我们将考虑线程池的设计。