[英]C++11 thread-safe queue
我正在處理的一個項目使用多個線程來處理一組文件。 每個線程都可以將文件添加到要處理的文件列表中,因此我將(我認為是)一個線程安全隊列放在一起。 相關部分如下:
// qMutex is a std::mutex intended to guard the queue
// populatedNotifier is a std::condition_variable intended to
// notify waiting threads of a new item in the queue
void FileQueue::enqueue(std::string&& filename)
{
std::lock_guard<std::mutex> lock(qMutex);
q.push(std::move(filename));
// Notify anyone waiting for additional files that more have arrived
populatedNotifier.notify_one();
}
std::string FileQueue::dequeue(const std::chrono::milliseconds& timeout)
{
std::unique_lock<std::mutex> lock(qMutex);
if (q.empty()) {
if (populatedNotifier.wait_for(lock, timeout) == std::cv_status::no_timeout) {
std::string ret = q.front();
q.pop();
return ret;
}
else {
return std::string();
}
}
else {
std::string ret = q.front();
q.pop();
return ret;
}
}
但是,我偶爾會在if (...wait_for(lock, timeout) == std::cv_status::no_timeout) { }
塊內出現段錯誤,並且 gdb 中的檢查表明由於隊列為空而發生段錯誤。 這怎么可能? 據我了解, wait_for
僅在收到通知時才返回cv_status::no_timeout
,並且這只應在FileQueue::enqueue
剛剛將新項目推送到隊列后發生。
最好使條件(由您的條件變量監控)成為 while 循環的逆條件: while(!some_condition)
。 在這個循環中,如果條件失敗,您將進入睡眠狀態,從而觸發循環體。
這樣,如果您的線程被喚醒(可能是虛假的),您的循環仍將在繼續之前檢查條件。 將條件視為感興趣的狀態,並將條件變量更多地視為來自系統的信號,表明該狀態可能已准備好。 循環將完成實際確認它是真實的繁重工作,如果不是,則進入睡眠狀態。
我剛剛為異步隊列編寫了一個模板,希望對您有所幫助。 在這里, q.empty()
是我們想要的相反條件:隊列中有東西。 所以它作為while循環的檢查。
#ifndef SAFE_QUEUE
#define SAFE_QUEUE
#include <queue>
#include <mutex>
#include <condition_variable>
// A threadsafe-queue.
template <class T>
class SafeQueue
{
public:
SafeQueue(void)
: q()
, m()
, c()
{}
~SafeQueue(void)
{}
// Add an element to the queue.
void enqueue(T t)
{
std::lock_guard<std::mutex> lock(m);
q.push(t);
c.notify_one();
}
// Get the "front"-element.
// If the queue is empty, wait till a element is avaiable.
T dequeue(void)
{
std::unique_lock<std::mutex> lock(m);
while(q.empty())
{
// release lock as long as the wait and reaquire it afterwards.
c.wait(lock);
}
T val = q.front();
q.pop();
return val;
}
private:
std::queue<T> q;
mutable std::mutex m;
std::condition_variable c;
};
#endif
根據標准condition_variables
被允許虛假喚醒,即使事件沒有發生。 在虛假喚醒的情況下,它會返回cv_status::no_timeout
(因為它是喚醒而不是超時),即使它沒有被通知。 正確的解決方案當然是在繼續之前檢查喚醒是否真的合法。
詳細信息在標准§30.5.1 [thread.condition.condvar] 中指定:
— 當調用 notify_one()、調用 notify_all()、abs_time 指定的絕對超時 (30.2.4) 到期或虛假發出信號時,該函數將解除阻塞。
...
返回:如果 abs_time 指定的絕對超時 (30.2.4) 已過期,則返回 cv_status::timeout,否則為 cv_status::no_timeout。
這可能是您應該這樣做的方式:
void push(std::string&& filename)
{
{
std::lock_guard<std::mutex> lock(qMutex);
q.push(std::move(filename));
}
populatedNotifier.notify_one();
}
bool try_pop(std::string& filename, std::chrono::milliseconds timeout)
{
std::unique_lock<std::mutex> lock(qMutex);
if(!populatedNotifier.wait_for(lock, timeout, [this] { return !q.empty(); }))
return false;
filename = std::move(q.front());
q.pop();
return true;
}
除了已接受的答案之外,我想說實現正確的多生產者/多消費者隊列很困難(不過,自 C++11 以來更容易)
我建議您嘗試(非常好的)無鎖 boost 庫,“隊列”結構將做您想做的事,具有無等待/無鎖保證並且不需要 C++11 編譯器。
我現在添加這個答案是因為無鎖庫對於提升來說是相當新的(我相信從 1.53 開始)
我會將您的出列函數重寫為:
std::string FileQueue::dequeue(const std::chrono::milliseconds& timeout)
{
std::unique_lock<std::mutex> lock(qMutex);
while(q.empty()) {
if (populatedNotifier.wait_for(lock, timeout) == std::cv_status::timeout )
return std::string();
}
std::string ret = q.front();
q.pop();
return ret;
}
它更短,並且沒有像您那樣重復的代碼。 僅發出它可能會等待更長的超時時間。 為防止您需要記住循環之前的開始時間,請檢查超時並相應地調整等待時間。 或指定等待條件的絕對時間。
這個案例也有 GLib 解決方案,我還沒試過,但我相信這是一個很好的解決方案。 https://developer.gnome.org/glib/2.36/glib-Asynchronous-Queues.html#g-async-queue-new
BlockingCollection是一個 C++11 線程安全的集合類,它提供對隊列、堆棧和優先級容器的支持。 它處理您描述的“空”隊列場景。 以及“完整”隊列。
你可能喜歡 lfqueue, https://github.com/Taymindis/lfqueue 。 它是無鎖並發隊列。 我目前正在使用它來消耗來自多個來電的隊列,並且像一個魅力一樣工作。
這是我在 C++20 中實現的線程隊列:
#pragma once
#include <deque>
#include <mutex>
#include <condition_variable>
#include <utility>
#include <concepts>
#include <list>
template<typename QueueType>
concept thread_queue_concept =
std::same_as<QueueType, std::deque<typename QueueType::value_type, typename QueueType::allocator_type>>
|| std::same_as<QueueType, std::list<typename QueueType::value_type, typename QueueType::allocator_type>>;
template<typename QueueType>
requires thread_queue_concept<QueueType>
struct thread_queue
{
using value_type = typename QueueType::value_type;
thread_queue();
explicit thread_queue( typename QueueType::allocator_type const &alloc );
thread_queue( thread_queue &&other );
thread_queue &operator =( thread_queue const &other );
thread_queue &operator =( thread_queue &&other );
bool empty() const;
std::size_t size() const;
void shrink_to_fit();
void clear();
template<typename ... Args>
requires std::is_constructible_v<typename QueueType::value_type, Args ...>
void enque( Args &&... args );
template<typename Producer>
requires requires( Producer producer ) { { producer() } -> std::same_as<std::pair<bool, typename QueueType::value_type>>; }
void enqueue_multiple( Producer producer );
template<typename Consumer>
requires requires( Consumer consumer, typename QueueType::value_type value ) { { consumer( std::move( value ) ) } -> std::same_as<bool>; }
void dequeue_multiple( Consumer consumer );
typename QueueType::value_type dequeue();
void swap( thread_queue &other );
private:
mutable std::mutex m_mtx;
mutable std::condition_variable m_cv;
QueueType m_queue;
};
template<typename QueueType>
requires thread_queue_concept<QueueType>
thread_queue<QueueType>::thread_queue()
{
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
thread_queue<QueueType>::thread_queue( typename QueueType::allocator_type const &alloc ) :
m_queue( alloc )
{
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
thread_queue<QueueType>::thread_queue( thread_queue &&other )
{
using namespace std;
lock_guard lock( other.m_mtx );
m_queue = move( other.m_queue );
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
thread_queue<QueueType> &thread_queue<QueueType>::thread_queue::operator =( thread_queue const &other )
{
std::lock_guard
ourLock( m_mtx ),
otherLock( other.m_mtx );
m_queue = other.m_queue;
return *this;
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
thread_queue<QueueType> &thread_queue<QueueType>::thread_queue::operator =( thread_queue &&other )
{
using namespace std;
lock_guard
ourLock( m_mtx ),
otherLock( other.m_mtx );
m_queue = move( other.m_queue );
return *this;
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
bool thread_queue<QueueType>::thread_queue::empty() const
{
std::lock_guard lock( m_mtx );
return m_queue.empty();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
std::size_t thread_queue<QueueType>::thread_queue::size() const
{
std::lock_guard lock( m_mtx );
return m_queue.size();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
void thread_queue<QueueType>::thread_queue::shrink_to_fit()
{
std::lock_guard lock( m_mtx );
return m_queue.shrink_to_fit();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
void thread_queue<QueueType>::thread_queue::clear()
{
std::lock_guard lock( m_mtx );
m_queue.clear();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
template<typename ... Args>
requires std::is_constructible_v<typename QueueType::value_type, Args ...>
void thread_queue<QueueType>::thread_queue::enque( Args &&... args )
{
using namespace std;
unique_lock lock( m_mtx );
m_queue.emplace_front( forward<Args>( args ) ... );
m_cv.notify_one();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
typename QueueType::value_type thread_queue<QueueType>::thread_queue::dequeue()
{
using namespace std;
unique_lock lock( m_mtx );
while( m_queue.empty() )
m_cv.wait( lock );
value_type value = move( m_queue.back() );
m_queue.pop_back();
return value;
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
template<typename Producer>
requires requires( Producer producer ) { { producer() } -> std::same_as<std::pair<bool, typename QueueType::value_type>>; }
void thread_queue<QueueType>::enqueue_multiple( Producer producer )
{
using namespace std;
lock_guard lock( m_mtx );
for( std::pair<bool, value_type> ret; (ret = move( producer() )).first; )
m_queue.emplace_front( move( ret.second ) ),
m_cv.notify_one();
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
template<typename Consumer>
requires requires( Consumer consumer, typename QueueType::value_type value ) { { consumer( std::move( value ) ) } -> std::same_as<bool>; }
void thread_queue<QueueType>::dequeue_multiple( Consumer consumer )
{
using namespace std;
unique_lock lock( m_mtx );
for( ; ; )
{
while( m_queue.empty() )
m_cv.wait( lock );
try
{
bool cont = consumer( move( m_queue.back() ) );
m_queue.pop_back();
if( !cont )
return;
}
catch( ... )
{
m_queue.pop_back();
throw;
}
}
}
template<typename QueueType>
requires thread_queue_concept<QueueType>
void thread_queue<QueueType>::thread_queue::swap( thread_queue &other )
{
std::lock_guard
ourLock( m_mtx ),
otherLock( other.m_mtx );
m_queue.swap( other.m_queue );
}
唯一的模板參數是 BaseType,它可以是 std::deque 類型或 std::list 類型,受 thread_queue_concept 限制。 此類使用此類型作為內部隊列類型。 選擇對您的應用程序最有效的 BaseType。 我可能已經將這個類限制在一個更區分的 thread_queue_concepts 上,它檢查 BaseType 的所有使用部分,以便這個類可能適用於與 std::list<> 或 std::deque<> 兼容的其他類型,但我懶得在不太可能的情況下實施,即有人自己實施類似的事情。 此代碼的一個優點是 enqueue_multiple 和 dequeue_multiple。 這些函數被賦予一個函數對象,通常是一個 lambda,它只需一個鎖定步驟就可以使多個項目入隊或出隊。 對於入隊,這始終成立,對於出隊,這取決於隊列是否有要獲取的元素。
如果您有一個生產者和多個消費者,則 enqueue_multiple 通常是有意義的。 它導致持有鎖的時間更長,因此只有在物品可以生產或快速移動時才有意義。
如果您有多個生產者和一個消費者,則 dequeue_multiple 通常是有意義的。 在這里我們也有更長的鎖定時間,但由於對象通常在這里只有快速移動,這通常不會造成傷害。
如果 dequeue_multiple 的消費者函數對象在消費時拋出異常,則異常被 caugt 並提供給消費者的元素(底層隊列類型對象內的右值引用)被刪除。
如果你想在 C++11 中使用這個類,你必須刪除這些概念或使用 #if defined(__cpp_concepts) 禁用它們。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.