[英]epoll IO with worker threads in C
我正在編寫一個小型服務器,它將從多個來源接收數據並處理這些數據。 收到的消息來源和數據非常重要,但epoll應該能夠很好地處理。 但是,必須解析所有接收到的數據並運行大量的測試,這些測試非常耗時,並且盡管進行了epoll多路復用,仍會阻塞單個線程。 基本上,模式應該如下所示:IO循環接收數據並將其捆綁到作業中,發送到池中可用的第一個線程,捆綁由作業處理,結果傳遞到IO循環寫入文件。
我決定選擇一個IO線程和N個工作線程。 使用以下示例提供的用於接受tcp連接和讀取數據的IO線程很容易實現: http : //linux.die.net/man/7/epoll
線程通常也很容易處理,但我正在努力將epoll IO循環與線程池以優雅的方式結合起來。 我無法找到任何與在線工作池使用epoll的“最佳實踐”,但有關同一主題的相關問題。
因此,我有一些問題,希望有人能幫我回答:
編輯:一種可能的解決方案是從IO循環更新環形緩沖區,更新后通過所有工作人員的共享管道向工作人員發送環形緩沖區索引(從而將該索引的控制權交給第一個讀取該索引的工作人員關閉管道索引),讓工人擁有該索引直到處理結束,然后再通過管道將索引號發送回IO線程,從而給予回控制?
我的應用程序僅限Linux,因此我可以使用僅限Linux的功能,以便以最優雅的方式實現這一目標。 不需要跨平台支持,但性能和線程安全性是必需的。
在我的測試中,每個線程一個epoll實例遠遠超過了復雜的線程模型。 如果將偵聽器套接字添加到所有epoll實例,則工作人員將accept(2)
並且獲勝者將被授予連接並在其生命周期內處理它。
你的工人看起來像這樣:
for (;;) {
nfds = epoll_wait(worker->efd, &evs, 1024, -1);
for (i = 0; i < nfds; i++)
((struct socket_context*)evs[i].data.ptr)->handler(
evs[i].data.ptr,
evs[i].events);
}
添加到epoll實例的每個文件描述符都可以有一個struct socket_context
關聯的struct socket_context
:
void listener_handler(struct socket_context* ctx, int ev)
{
struct socket_context* conn;
conn->fd = accept(ctx->fd, NULL, NULL);
conn->handler = conn_handler;
/* add to calling worker's epoll instance or implement some form
* of load balancing */
}
void conn_handler(struct socket_context* ctx, int ev)
{
/* read all available data and process. if incomplete, stash
* data in ctx and continue next time handler is called */
}
void dummy_handler(struct socket_context* ctx, int ev)
{
/* handle exit condition async by adding a pipe with its
* own handler */
}
我喜歡這個策略,因為:
read(2)
; accept(2)
上的同步); accept(2)
所以有點自然負載平衡。 關於epoll的一些注釋:
EAGAIN
; dup(2)
系列調用來避免一些意外(epoll寄存器文件描述符 ,但實際上是監視文件描述 ); epoll_ctl(2)
其他線程的epoll實例; epoll_wait(2)
使用一個大的struct epoll_event
緩沖區來避免飢餓。 其他一些說明:
accept4(2)
保存系統調用; poll(2)
/ select(2)
可能會更快。 我希望這有幫助。
執行此模型時,因為我們只有在完全接收到數據包后才知道數據包大小,遺憾的是我們無法將接收本身卸載到工作線程。 相反,我們仍然可以做的最好的事情是接收數據的線程,該數據必須將指針傳遞給完全接收的數據包。
數據本身可能最好保存在循環緩沖區中,但是我們需要為每個輸入源提供一個單獨的緩沖區(如果我們得到一個部分數據包,我們可以繼續從其他來源接收而不分割數據。剩下的問題是如何通知新數據包准備就緒的工作者,並給他們一個指向所述數據包中數據的指針。由於這里的數據很少,只有一些指針,最優雅的方法是使用posix消息隊列。這些提供了多個發送者和多個接收者能夠寫入和讀取消息的能力,始終確保每個消息都被接收並且精確地通過1個線程。
對於每個數據源,您將需要一個類似下面的結構,我現在將通過字段目的。
struct DataSource
{
int SourceFD;
char DataBuffer[MAX_PACKET_SIZE * (THREAD_COUNT + 1)];
char *LatestPacket;
char *CurrentLocation
int SizeLeft;
};
SourceFD顯然是有問題的數據流的文件描述符,DataBuffer是處理包時內容的地方,它是一個循環緩沖區。 LatestPacket指針用於臨時保存指向最重新發送的數據包的指針,以防我們收到部分數據包並在關閉數據包之前移動到另一個源。 CurrentLocation存儲最新數據包的結束位置,以便我們知道下一個數據包的放置位置或部分接收的位置。 剩下的大小是緩沖區中留下的空間,這將用於判斷我們是否可以適應數據包或需要繞回到開頭。
因此接收功能將有效
工作線程將使用接收的指針進行處理,然后增加SizeLeft,以便接收器線程知道它可以繼續填充緩沖區。 原子函數將需要處理結構中的大小值,因此我們不會獲得具有size屬性的競爭條件(因為它可能由工作者和IO線程同時寫入,導致丟失的寫入,請參閱我的在下面評論),它們在這里列出並且簡單且非常有用。
現在,我已經給出了一些一般背景,但將具體說明給出的要點:
最后你的編輯是相當明智的,除了我提出的事實,消息隊列遠比管道好,因為它們非常有效地發出事件信號,保證完整的消息讀取並提供自動框架。
我希望這有所幫助,但是它已經很晚了,所以如果我錯過了任何問題,或者您有任何問題可以隨意評論澄清或更多解釋。
我在其他帖子中發布了相同的答案: 我想等待文件描述符和互斥量,建議的方法是什么?
================================================== ========
這是一個非常常見的問題,尤其是在開發網絡服務器端程序時。 大多數Linux服務器端程序的主要外觀將像這樣循環:
epoll_add(serv_sock);
while(1){
ret = epoll_wait();
foreach(ret as fd){
req = fd.read();
resp = proc(req);
fd.send(resp);
}
}
它是單線程(主線程),基於epoll的服務器框架。 問題是,它是單線程的,而不是多線程的。 它要求proc()永遠不會阻塞或運行很長時間(例如,對於常見情況,為10毫秒)。
如果proc()將運行很長時間,我們需要MULTI THREADS,並在一個單獨的線程(工作線程)中執行proc()。
我們可以在不阻塞主線程的情況下向工作線程提交任務,使用基於互斥鎖的消息隊列,它足夠快。
然后我們需要一種從工作線程獲取任務結果的方法。 怎么樣? 如果我們只是在epoll_wait()之前或之后直接檢查消息隊列,但是,在epoll_wait()結束后執行檢查操作,如果等待所有文件描述符,則epoll_wait()通常會阻塞10微秒(常見情況)不活躍。
對於服務器,10毫秒是相當長的時間! 我們可以發信號通知epoll_wait()在生成任務結果時立即結束嗎?
是! 我將在我的一個開源項目中描述它是如何完成的。
為所有工作線程創建管道,epoll也在該管道上等待。 生成任務結果后,工作線程將一個字節寫入管道,然后epoll_wait()將在幾乎相同的時間內結束! - Linux管道有5到20美元的延遲。
在我的項目SSDB (兼容Redis協議的磁盤NoSQL數據庫)中,我創建了一個SelectableQueue,用於在主線程和工作線程之間傳遞消息。 就像它的名字一樣,SelectableQueue有一個文件描述符,可以通過epoll等待。
SelectableQueue: https : //github.com/ideawu/ssdb/blob/master/src/util/thread.h#L94
在主線程中的用法:
epoll_add(serv_sock);
epoll_add(queue->fd());
while(1){
ret = epoll_wait();
foreach(ret as fd){
if(fd is worker_thread){
sock, resp = worker->pop_result();
sock.send(resp);
}
if(fd is client_socket){
req = fd.read();
worker->add_task(fd, req);
}
}
}
工作線程中的用法:
fd, req = queue->pop_task();
resp = proc(req);
queue->add_result(fd, resp);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.