簡體   English   中英

動態生成和安全使用spsc_queues

[英]Dynamic generation & safe usage of spsc_queues

我所做的唯一的boost::lockfreespsc_queue ,這太神奇了。

但是,我想在一個線程與cores - 1線程來回傳遞信息的地方實現它。

我當時在想,每個工作線程都會有自己的spsc_queues集合,該集合將被存儲在vector s中,在那里主線程將信息傳遞到一個傳出隊列,然后移動到vector的下一個隊列,並且等等,以及循環進入的隊列。

可以安全地推送和彈出兩個vector的這些spsc_queue嗎?

如果不是,是否有根據我的意圖使用spsc_queues的替代方法?

基本上,您建議以預期的方式使用2x(cores-1)spsc_queues。 是的,這行得通。

我不明白你怎么會明顯地在主線程中處理響應(“傳入隊列”)雖然。 實話實說,傳入隊列上沒有“等待”操作,您也不想要一個(不再是非常無鎖的,並且在等待傳入消息時,所有其他工作人員都不會得到服務)。

撇開 :如果您確定響應隊列的大小,使其永遠不會溢出,那么您可以通過它從幼稚的輪循機制中讀取內容(如果不要嘗試從單個響應隊列中讀取所有消息,則可以走很長一段路) -fire方法來獲取其他響應隊列的調度不足)。

底部的代碼樣本( CODE SAMPLE

所有這些使我強烈懷疑您實際上是異步之后,而不是在並發之后 我有一種讓您的應用程序在1個線程上運行的感覺,這是非常高興的,只是盡快“盡可能地”服務每條可用消息-無論消息的來源或內容如何。

  1. 對於可以在極短時間內處理[**] **的大量小消息,此模型將很好地擴展。
  2. 當1個線程飽和時,您可以通過添加worker來擴展。
  3. 在具有需要大量處理消息的消息的服務中,您可以以異步方式在僅處理低頻請求的專用線程上分擔這些任務:它們可以將小的“完成”消息一旦發送回主工作隊列即可'重做。

所有這些使我想到了libuv或Boost Asio之類的庫。 如果您已經一手掌握了要獲得所需吞吐量就需要無鎖運行的方法(這在工業強度服務器解決方案中很少見),則可以使用無鎖隊列進行模擬。 這需要更多工作,因為您必須將epoll / select / poll循環集成到生產者中。 我建議您保持簡單,簡單,並僅在實際需要時才采用附加的復雜性。

口頭禪:正確,考究; 稍后優化

(請注意此處的“構造良好”。在這種情況下,這意味着您將/不/允許在高吞吐量隊列上執行緩慢的處理任務。)

代碼樣本

如所承諾的,一個簡單的概念證明顯示了使用帶有多個工作線程的多個雙向SPSC隊列消息傳遞。

完全無鎖定的版本: Live on Coliru

這里有很多微妙之處。 特別要注意的是,隊列不足如何導致靜默丟棄的消息。 如果消費者能夠跟上生產者的步伐,這將不會發生,但是只要您不知道操作系統的活動,就應該為此添加檢查。

更新根據注釋中的請求,這是一個檢查隊列是否飽和的版本-不丟棄消息。 也可以在Coliru上觀看

  • 不能刪除任何消息
  • 沒有更多的延遲到達(因為直到收到所有響應后主循環才退出)
  • 循環不再綁定到循環變量,因為發送可能會停頓,這將導致始終讀取相同的響應隊列。 這是死鎖或其他最壞情況下性能的秘訣。
  • 在隊列飽和的情況下,我們必須考慮一種平衡負載的適當方法。 我選擇了小睡。 從技術上講,這意味着當隊列飽和時,我們的無鎖免 等待解決方案將降級為常規協作多線程。 如果檢測到這種情況,也許您希望增加隊列。 這完全取決於您的系統。
  • 您將想知道什么時候發生; 我包括了所有線程的簡單擁塞統計信息。 在我的系統上,通過microsleep調用sleep_for(nanoseconds(1)) ,輸出為:

     Received 1048576 responses (97727 100529 103697 116523 110995 115291 103048 102611 102583 95572 ) Total: 1048576 responses/1048576 requests Main thread congestion: 21.2% Worker #0 congestion: 1.7% Worker #1 congestion: 3.1% Worker #2 congestion: 2.0% Worker #3 congestion: 2.5% Worker #4 congestion: 4.5% Worker #5 congestion: 2.5% Worker #6 congestion: 3.0% Worker #7 congestion: 3.2% Worker #8 congestion: 3.1% Worker #9 congestion: 3.6% real 0m0.616s user 0m3.858s sys 0m0.025s 

    如您所見,Coliru的調音必須大不相同。 只要您的系統存在以最大負載運行的風險,就需要進行此調整。

  • 相反,您必須考慮在隊列為空時如何限制負載:此刻,工作人員將僅在隊列上忙循環,等待消息出現。 在真實的服務器環境中,當負載突然爆發時,您將需要檢測“空閑”周期並降低輪詢頻率,以節省CPU功耗(同時允許CPU最大化其他線程的吞吐量)。

此答案中包括第二個“混合”版本(在隊列飽和之前是無鎖的):

#include <boost/lockfree/spsc_queue.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/thread.hpp>
#include <memory>
#include <iostream>
#include <iterator>

namespace blf = boost::lockfree;

static boost::atomic_bool shutdown(false);

static void nanosleep()
{
    //boost::this_thread::yield();
    boost::this_thread::sleep_for(boost::chrono::nanoseconds(1));
}

struct Worker
{
    typedef blf::spsc_queue<std::string > queue;
    typedef std::unique_ptr<queue> qptr;
    qptr incoming, outgoing;
    size_t congestion = 0;

    Worker() : incoming(new queue(64)), outgoing(new queue(64)) 
    {
    }

    void operator()()
    {
        std::string request;
        while (!shutdown)
        {
            while (incoming->pop(request)) 
                while (!outgoing->push("Ack: " + request))
                    ++congestion, nanosleep();
        }
    }
};

int main()
{
    boost::thread_group g;

    std::vector<Worker> workers(10);
    std::vector<size_t> responses_received(workers.size());

    for (auto& w : workers)
        g.create_thread(boost::ref(w));

    // let's give them something to do
    const auto num_requests = (1ul<<20);
    std::string response;
    size_t congestion = 0;

    for (size_t total_sent = 0, total_received = 0; total_sent < num_requests || total_received < num_requests;)
    {
        if (total_sent < num_requests)
        {
            // send to a random worker
            auto& to = workers[rand() % workers.size()];
            if (to.incoming->push("request " + std::to_string(total_sent)))
                ++total_sent;
            else
                congestion++;
        }

        if (total_received < num_requests)
        {
            static size_t round_robin = 0;
            auto from = (++round_robin) % workers.size();
            if (workers[from].outgoing->pop(response))
            {
                ++responses_received[from];
                ++total_received;
            }
        }
    }

    auto const sum = std::accumulate(begin(responses_received), end(responses_received), size_t());
    std::cout << "\nReceived " << sum << " responses (";
    std::copy(begin(responses_received), end(responses_received), std::ostream_iterator<size_t>(std::cout, " "));
    std::cout << ")\n";

    shutdown = true;
    g.join_all();

    std::cout << "\nTotal: " << sum << " responses/" << num_requests << " requests\n";

    std::cout << "Main thread congestion: " << std::fixed << std::setprecision(1) << (100.0*congestion/num_requests) << "%\n";

    for (size_t idx = 0; idx < workers.size(); ++idx)
        std::cout << "Worker #" << idx << " congestion: " << std::fixed << std::setprecision(1) << (100.0*workers[idx].congestion/responses_received[idx]) << "%\n";
}

[¹] “非常少的時間”一如既往是一個相對的概念,大致意味着“比新消息之間的平均時間短的時間”。 例如,如果您每秒有100個請求,那么對於單線程系統,5ms的處理時間將“非常少”。 但是,如果每秒有1萬個請求,則1毫秒的處理時間大約是16核服務器上的限制。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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