簡體   English   中英

C++11 線程安全隊列

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM