[英]Which is a better architecture? Multiple SPSC queues or a one MPSC queue?
[英]Dynamic generation & safe usage of spsc_queues
我所做的唯一的boost::lockfree
是spsc_queue
,這太神奇了。
但是,我想在一個線程與cores - 1
線程來回傳遞信息的地方實現它。
我當時在想,每個工作線程都會有自己的spsc_queues
集合,該集合將被存儲在vector
s中,在那里主線程將信息傳遞到一個傳出隊列,然后移動到vector
的下一個隊列,並且等等,以及循環進入的隊列。
可以安全地推送和彈出兩個vector
的這些spsc_queue
嗎?
如果不是,是否有根據我的意圖使用spsc_queues的替代方法?
基本上,您建議以預期的方式使用2x(cores-1)spsc_queues。 是的,這行得通。
我不明白你怎么會明顯地在主線程中處理響應(“傳入隊列”)雖然。 實話實說,傳入隊列上沒有“等待”操作,您也不想要一個(不再是非常無鎖的,並且在等待傳入消息時,所有其他工作人員都不會得到服務)。
撇開 :如果您確定響應隊列的大小,使其永遠不會溢出,那么您可以通過它從幼稚的輪循機制中讀取內容(如果不要嘗試從單個響應隊列中讀取所有消息,則可以走很長一段路) -fire方法來獲取其他響應隊列的調度不足)。
底部的代碼樣本( CODE SAMPLE )
所有這些使我強烈懷疑您實際上是在異步之后,而不是在並發之后 。 我有一種讓您的應用程序在1個線程上運行的感覺,這是非常高興的,只是盡快“盡可能地”服務每條可用消息-無論消息的來源或內容如何。
所有這些使我想到了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.