Linux網絡編程 - 子線程使用poll處理連接 I/O事件(高併發高性能進階篇)

這一篇我們就將 acceptor 上的連接建立事件和已建立連接的 I/O 事件分離,形成所謂的主 - 從 reactor 模式

主 - 從 reactor 模式

主 - 從這個模式的核心思想是,主反應堆線程只負責分發 Acceptor 連接建立,已連接套接字上的 I/O 事件交給 sub-reactor 負責分發。其中 sub-reactor 的數量,可以根據 CPU 的核數來靈活設置

多個反應堆線程同時在工作,這大大增強了 I/O 分發處理的效率,並且同一個套接字事件分發只會出現在一個反應堆線程中,這會大大減少併發處理的鎖開銷。

                                              

來解釋一下這張圖,我們的主反應堆線程一直在感知連接建立的事件,如果有連接成功建立,主反應堆線程通過 accept 方法獲取已連接套接字,接下來會按照一定的算法選取一個從反應堆線程,並把已連接套接字加入到選擇好的從反應堆線程中

主反應堆線程唯一的工作,就是調用 accept 獲取已連接套接字,以及將已連接套接字加入到從反應堆線程中。不過,這裏還有一個小問題,主反應堆線程和從反應堆線程,是兩個不同的線程,如何把已連接套接字加入到另外一個線程中呢?這是高性能網絡程序框架要解決的問題,在後面,將會給出這個問題的答案。

主 - 從 reactor+worker threads 模式

如果說主 - 從 reactor 模式解決了 I/O 分發的高效率問題,那麼 work threads 就解決了業務邏輯和 I/O 分發之間的耦合問題。把這兩個策略組裝在一起,就是實戰中普遍採用的模式。大名鼎鼎的 Netty,就是把這種模式發揮到極致的一種實現。不過要注意 Netty 裏面提到的 worker 線程,其實就是我們這裏說的從 reactor 線程,並不是處理具體業務邏輯的 worker 線程。

下面貼的一段代碼就是常見的 Netty 初始化代碼,這裏 Boss  Group 就是 acceptor 主反應堆,workerGroup 就是從反應堆。而處理業務邏輯的線程,通常都是通過使用 Netty 的程序開發者進行設計和定製,一般來說,業務邏輯線程需要從 workerGroup 線程中分離,以便支持更高的併發度。

public final class TelnetServer {
    static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8992" : "8023"));

    public static void main(String[] args) throws Exception {
        //產生一個reactor線程,只負責accetpor的對應處理
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        //產生一個reactor線程,負責處理已連接套接字的I/O事件分發
        EventLoopGroup workerGroup = new NioEventLoopGroup(1);
        try {
           //標準的Netty初始,通過serverbootstrap完成線程池、channel以及對應的handler設置,注意這裏講bossGroup和workerGroup作爲參數設置
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new TelnetServerInitializer(sslCtx));

            //開啓兩個reactor線程無限循環處理
            b.bind(PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

                                         

這張圖解釋了主 - 從反應堆下加上 worker 線程池的處理模式。主 - 從反應堆跟上面介紹的做法是一樣的。和上面不一樣的是,這裏將 decode、compute、encode 等 CPU 密集型的工作從 I/O 線程中拿走,這些工作交給 worker 線程池來處理,而且這些工作拆分成了一個個子任務進行。encode 之後完成的結果再由 sub-reactor 的 I/O 線程發送出去。

樣例程序

#include "lib/acceptor.h"
#include "lib/common.h"
#include "lib/event_loop.h"
#include "lib/tcp_server.h"

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

//連接建立之後的callback
int onConnectionCompleted(struct tcp_connection *tcpConnection) {
    printf("connection completed\n");
    return 0;
}

//數據讀到buffer之後的callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
    printf("get message from tcp connection %s\n", tcpConnection->name);
    printf("%s", input->data);

    struct buffer *output = buffer_new();
    int size = buffer_readable_size(input);
    for (int i = 0; i < size; i++) {
        buffer_append_char(output, rot13_char(buffer_read_char(input)));
    }
    tcp_connection_send_buffer(tcpConnection, output);
    return 0;
}

//數據通過buffer寫完之後的callback
int onWriteCompleted(struct tcp_connection *tcpConnection) {
    printf("write completed\n");
    return 0;
}

//連接關閉之後的callback
int onConnectionClosed(struct tcp_connection *tcpConnection) {
    printf("connection closed\n");
    return 0;
}

int main(int c, char **v) {
    //主線程event_loop
    struct event_loop *eventLoop = event_loop_init();

    //初始化acceptor
    struct acceptor *acceptor = acceptor_init(SERV_PORT);

    //初始tcp_server,可以指定線程數目,這裏線程是4,說明是一個acceptor線程,4個I/O線程,沒一個I/O線程
    //tcp_server自己帶一個event_loop
    struct TCPserver *tcpServer = tcp_server_init(eventLoop, acceptor, onConnectionCompleted, onMessage,
                                                  onWriteCompleted, onConnectionClosed, 4);
    tcp_server_start(tcpServer);

    // main thread for acceptor
    event_loop_run(eventLoop);
}

代碼中lib目錄下的代碼,自己去查看 https://github.com/froghui/yolanda。樣例程序幾乎和上一篇的一樣,唯一的不同是在創建 TCPServer 時,線程的數量設置不再是 0,而是 4。這裏線程是 4,說明是一個主 acceptor 線程,4 個從 reactor 線程,每一個線程都跟一個 event_loop 一一綁定。

你可能會問,這麼簡單就完成了主、從線程的配置?這其實是設計框架需要考慮的地方,一個框架不僅要考慮性能、擴展性,也需要考慮可用性。可用性部分就是程序開發者如何使用框架。如果我是一個開發者,我肯定關心框架的使用方式是不是足夠方便,配置是不是足夠靈活等。

像這裏,可以根據需求靈活地配置主、從反應堆線程,就是一個易用性的體現。當然,因爲時間有限,我沒有考慮 woker 線程的部分,這部分其實應該是應用程序自己來設計考慮。網絡編程框架通過回調函數暴露了交互的接口,這裏應用程序開發者完全可以在 onMessage 方法裏面獲取一個子線程來處理 encode、compute 和 encode 的工作,像下面的示範代碼一樣。

//數據讀到buffer之後的callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
    printf("get message from tcp connection %s\n", tcpConnection->name);
    printf("%s", input->data);
    //取出一個線程來負責decode、compute和encode
    struct buffer *output = thread_handle(input);
    //處理完之後再通過reactor I/O線程發送數據
    tcp_connection_send_buffer(tcpConnection, output);
    return 

 

 

溫故而知新 !

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章