簡體   English   中英

epoll IO與C中的工作線程

[英]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的“最佳實踐”,但有關同一主題的相關問題。

因此,我有一些問題,希望有人能幫我回答:

  1. 可以(並且應該)使用eventfd作為IO線程和所有工作者之間雙向同步的機制嗎? 例如,每個工作線程是否有一個好主意讓自己的epoll例程等待共享的eventfd(帶有結構指針,包含有關作業的數據/信息),即以某種方式使用eventfd作為作業隊列? 也許還有另一個eventfd將結果從多個工作線程傳遞回IO線程?
  2. 在IO線程上發出關於套接字的更多數據的信號之后,實際的recv應該發生在IO線程上,還是工作者應該自己重新獲取數據,以便在解析數據幀時不阻塞IO線程等? 在這種情況下,我如何確保安全性,例如,如果recv在工作線程中讀取1,5幀數據而另一個工作線程從同一連接接收最后0.5幀數據?
  3. 如果工作線程池是通過互斥鎖等實現的,如果N + 1個線程試圖使用相同的鎖,那么等待鎖會阻塞IO線程嗎?
  4. 對於如何使用雙向通信(即從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)保存系統調用;
  • 每個核心使用一個線程(如果CPU綁定,每個物理1個,如果I / O綁定,則每個邏輯使用1個);
  • 如果連接數低, 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存儲最新數據包的結束位置,以便我們知道下一個數據包的放置位置或部分接收的位置。 剩下的大小是緩沖區中留下的空間,這將用於判斷我們是否可以適應數據包或需要繞回到開頭。

因此接收功能將有效

  • 將數據包的內容復制到緩沖區中
  • 將CurrentLocation移動到指向數據包的末尾
  • 更新SizeLeft以考慮現在減少的緩沖區
  • 如果我們無法將數據包放在緩沖區的末尾,我們就會循環
  • 如果那里沒有空間,我們要稍后再試一次,同時去另一個來源
  • 如果我們有一個部分接收存儲,則LatestPacket指針指向數據包的開始並轉到另一個流,直到我們得到其余的
  • 使用posix消息隊列向工作線程發送消息,以便它可以處理數據,消息將包含指向DataSource結構的指針,以便它可以在其上工作,它還需要指向它正在處理的數據包的指針,以及它的大小,這些可以在我們收到數據包時計算出來

工作線程將使用接收的指針進行處理,然后增加SizeLeft,以便接收器線程知道它可以繼續填充緩沖區。 原子函數將需要處理結構中的大小值,因此我們不會獲得具有size屬性的競爭條件(因為它可能由工作者和IO線程同時寫入,導致丟失的寫入,請參閱我的在下面評論),它們在這里列出並且簡單且非常有用。

現在,我已經給出了一些一般背景,但將具體說明給出的要點:

  1. 使用EventFD作為同步機制在很大程度上是一個壞主意,你會發現自己使用了相當多的不必要的CPU時間,並且很難執行任何同步。 特別是如果你有多個線程選擇相同的文件描述符,你可能會遇到重大問題。 這實際上是一個討厭的黑客,有時會工作,但不能真正替代正確的同步。
  2. 如上所述嘗試卸載接收也是一個壞主意,你可以解決復雜IPC的問題,但坦率地說,接收IO不太可能花費足夠的時間來停止應用程序,你的IO也可能比CPU慢得多因此,使用多線程接收將獲得很少。 (假設您沒有說,有幾個10千兆網卡)。
  3. 使用互斥鎖或鎖是一個愚蠢的想法,它適用於無鎖編碼,因為(同時)共享數據量很少,你實際上只是在處理工作和數據。 這也將提高接收線程的性能,使您的應用程序更具可擴展性。 使用這里提到的功能http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html,你可以很容易地做到這一點。 如果你這樣做的話,你需要的是一個信號量,這可以在每次接收到一個數據包並被每個線程鎖定時解鎖,這個線程啟動一個作業,以便在更多數據包准備就緒時允許動態更多線程,這將有使用互斥鎖的自制解決方案遠遠少於開銷。
  4. 這里與任何線程池沒有太大區別,你產生了很多線程然后讓它們全部阻塞在數據消息隊列上的mq_receive中以等待消息。 完成后,他們將結果發送回主線程,主線程將結果消息隊列添加到其epoll列表中。 然后,它可以以這種方式接收結果,對於像指針這樣的小數據有效載荷,它非常簡單且非常有效。 這也將使用很少的CPU,而不是強迫主線程浪費時間管理工作人員。

最后你的編輯是相當明智的,除了我提出的事實,消息隊列遠比管道好,因為它們非常有效地發出事件信號,保證完整的消息讀取並提供自動框架。

我希望這有所幫助,但是它已經很晚了,所以如果我錯過了任何問題,或者您有任何問題可以隨意評論澄清或更多解釋。

我在其他帖子中發布了相同的答案: 我想等待文件描述符和互斥量,建議的方法是什么?

================================================== ========

這是一個非常常見的問題,尤其是在開發網絡服務器端程序時。 大多數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.

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