Reactor執行緒模型及其在Netty中的應用

zhong0316發表於2019-02-24

什麼是Reactor執行緒模型

Java中執行緒模型大致可以分為:

  1. 單執行緒模型
  2. 多執行緒模型
  3. 執行緒池模型(executor)
  4. Reactor執行緒模型

單執行緒模型中,server端使用一個執行緒來處理所有的請求,所有的請求必須序列化處理,效率低下。 多執行緒模型中,server端會為每個請求分配一個執行緒去處理請求,相對單執行緒模型而言多執行緒模型效率更高,但是多執行緒模型的缺點也很明顯:server端為每個請求都開闢一個執行緒來處理請求,如果請求數量很大,則會造成大量執行緒被建立,造成記憶體溢位。Java中執行緒是比較昂貴的物件。執行緒的數量不應該無限量的增大,當執行緒數超過一定數目後,增加執行緒不僅不能提高效率,反而會降低效率。 藉助"物件複用"的思想,執行緒池應運而生,執行緒池中一般有一定數目的執行緒,當請求數目超過執行緒數之後需要排隊等待。這樣就避免了執行緒會不停地增長。這裡不打算對Java的執行緒池做過多介紹,有興趣的可以去看我之前的文章:java執行緒池

Reactor是一種處理模式。Reactor模式是處理併發I/O比較常見的一種模式,用於同步I/O,中心思想是將所有要處理的IO事件註冊到一箇中心I/O多路複用器上,同時主執行緒/程式阻塞在多路複用器上;一旦有I/O事件到來或是準備就緒(檔案描述符或socket可讀、寫),多路複用器返回並將事先註冊的相應I/O事件分發到對應的處理器中。

Reactor也是一種實現機制。Reactor利用事件驅動機制實現,和普通函式呼叫的不同之處在於:應用程式不是主動的呼叫某個API完成處理,而是恰恰相反,Reactor逆置了事件處理流程,應用程式需要提供相應的介面並註冊到Reactor上,如果相應的事件發生,Reactor將主動呼叫應用程式註冊的介面,這些介面又稱為“回撥函式”。用“好萊塢原則”來形容Reactor再合適不過了:不要打電話給我們,我們會打電話通知你。

為什麼需要Reactor模型

Reactor模型實質上是對I/O多路複用的一層包裝,理論上來說I/O多路複用的效率已經夠高了,為什麼還需要Reactor模型呢?答案是I/O多路複用雖然效能已經夠高了,但是編碼複雜,在工程效率上還是太低。因此出現了Reactor模型。

一個個網路請求可能涉及到多個I/O請求,相比傳統的單執行緒完整處理請求生命期的方法,I/O複用在人的大腦思維中並不自然,因為,程式設計師程式設計中,處理請求A的時候,假定A請求必須經過多個I/O操作A1-An(兩次IO間可能間隔很長時間),每經過一次I/O操作,再呼叫I/O複用時,I/O複用的呼叫返回裡,非常可能不再有A,而是返回了請求B。即請求A會經常被請求B打斷,處理請求B時,又被C打斷。這種思維下,程式設計容易出錯。

Reactor模型

一般來說處理一個網路請求需要經過以下五個步驟:

  1. 讀取請求資料(read request)
  2. 解碼資料(decode request)
  3. 計算,生成響應(compute)
  4. 編碼響應(encode response)
  5. 傳送響應資料(send response) 如下圖所示:
ClassicServiceDesign
不難看出,上面的模型中讀取請求資料和傳送響應和業務處理邏輯耦合在一起,都是用一個handler執行緒處理,當發生讀寫事件時執行緒將被阻塞,無法處理其他的事情。既然不能耦合在一起,那自然解決方案是將讀寫操作和其他三個步驟進行分離:有專門的執行緒或者執行緒池負責連線請求,然後使用多路複用器來監測Socket上的讀寫事件,其他的處理委派給業務執行緒池進行處理。讀寫操作不阻塞主執行緒。

Reactor模型有三種執行緒模型:

  1. 單執行緒模型
  2. 多執行緒模型(單Reactor)
  3. 多執行緒模型(多Reactor)

單執行緒Reactor模型

單執行緒模型中Reactor既負責Accept新的連線請求,又負責分派請求到具體的handler中進行處理,一般不使用這種模型,因為單執行緒效率比較低下。

BasicReactorPattern

下面是基於Java NIO單執行緒Reactor模型的實現:

class Reactor implements Runnable {
    final Selector selector;
    final ServerSocketChannel serverSocket;

    Reactor(int port) throws IOException { // Reactor設定
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(
                new InetSocketAddress(port));
        serverSocket.configureBlocking(false);
        SelectionKey sk =
                serverSocket.register(selector,
                        SelectionKey.OP_ACCEPT); // 監聽Socket連線事件
        sk.attach(new Acceptor());
    }

    public void run() { // normally in a new Thread
        try {
            while (!Thread.interrupted()) {
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while (it.hasNext())
                    dispatch((SelectionKey) (it.next());
                selected.clear();
            }
        } catch (IOException ex) { /* ... */ }
    }

    void dispatch(SelectionKey k) {
        Runnable r = (Runnable) (k.attachment());
        if (r != null)
            r.run();
    }

    class Acceptor implements Runnable { // inner
        public void run() {
            try {
                SocketChannel c = serverSocket.accept();
                if (c != null)
                    new Handler(selector, c);
            } catch (IOException ex) { /* ... */ }
        }
    }
}


final class Handler implements Runnable {
    static final int READING = 0, SENDING = 1;
    final SocketChannel socket;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(1024);
    ByteBuffer output = ByteBuffer.allocate(1024);
    int state = READING;

    Handler(Selector sel, SocketChannel c)
            throws IOException {
        socket = c;
        c.configureBlocking(false);
        // Optionally try first read now
        sk = socket.register(sel, 0);
        sk.attach(this);
        sk.interestOps(SelectionKey.OP_READ);
        sel.wakeup();
    }

    boolean inputIsComplete() { /* ... */ }

    boolean outputIsComplete() { /* ... */ }

    void process() { /* ... */ }

    public void run() {
        try {
            if (state == READING) read();
            else if (state == SENDING) send();
        } catch (IOException ex) { /* ... */ }
    }

    void read() throws IOException {
        socket.read(input);
        if (inputIsComplete()) {
            process();
            state = SENDING;
            // Normally also do first write now
            sk.interestOps(SelectionKey.OP_WRITE);
        }
    }

    void send() throws IOException {
        socket.write(output);
        if (outputIsComplete()) sk.cancel();
    }
}
複製程式碼

多執行緒模型(單Reactor)

該模型在事件處理器(Handler)鏈部分採用了多執行緒(執行緒池),也是後端程式常用的模型。模型圖如下:

WorkerThreadPools
其實現大致程式碼如下: ``` class Handler implements Runnable { // 使用執行緒池 static PooledExecutor pool = new PooledExecutor(...); static final int PROCESSING = 3; // ... synchronized void read() { // ... socket.read(input); if (inputIsComplete()) { state = PROCESSING; pool.execute(new Processer()); } } synchronized void processAndHandOff() { process(); state = SENDING; // or rebind attachment sk.interest(SelectionKey.OP_WRITE); } class Processer implements Runnable { public void run() { processAndHandOff(); } } } ```

多執行緒模型(多Reactor)

比起多執行緒單Rector模型,它是將Reactor分成兩部分,mainReactor負責監聽並Accept新連線,然後將建立的socket通過多路複用器(Acceptor)分派給subReactor。subReactor負責多路分離已連線的socket,讀寫網路資料;業務處理功能,其交給worker執行緒池完成。通常,subReactor個數上可與CPU個數等同。其模型圖如下:

UsingMultiplyReactors

Netty執行緒模型

Netty的執行緒模型類似於Reactor模型。Netty中ServerBootstrap用於建立服務端,下圖是它的結構:

Netty-ServerBootstrap
ServerBootstrap繼承自AbstractBootstrap,AbstractBootstrap中的group屬性就是Netty中的Acceptor,用於接受請求。而ServerBootstrap中的childGroup對應於Reactor模型中的worker執行緒池。 請求過來後Netty從group執行緒池中選出一個執行緒來建立連線,連線建立後委派給childGroup中的worker執行緒處理。 服務端執行緒模型工作原理如下圖:
Netty服務端執行緒工作流程

下面是一個完整的Netty服務端的例子:

public class TimeServer {
    public void bind(int port) {
        // Netty的多Reactor執行緒模型,bossGroup是Acceptor執行緒池,用於接受連線。workGroup是Worker執行緒池,處理業務。
        // bossGroup是Acceptor執行緒池
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // workGroup是Worker執行緒池
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childHandler(new ChildChannelHandler());
            // 繫結埠,同步等待成功
            ChannelFuture f = b.bind(port).sync();
            // 等待服務端監聽埠關閉
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
    private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline().addLast(new TimeServerHandler());
        }
    }
}

public class TimeServerHandler extends ChannelHandlerAdapter {

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // msg轉Buf
        ByteBuf buf = (ByteBuf) msg;
        // 建立緩衝中位元組數的位元組陣列
        byte[] req = new byte[buf.readableBytes()];
        // 寫入陣列
        buf.readBytes(req);
        String body = new String(req, "UTF-8");
        String currenTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(
                System.currentTimeMillis()).toString() : "BAD ORDER";
        // 將要返回的資訊寫入Buffer
        ByteBuf resp = Unpooled.copiedBuffer(currenTime.getBytes());
        // buffer寫入通道
        ctx.write(resp);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // write讀入緩衝陣列後通過invoke flush寫入通道
        ctx.flush();
    }
}
複製程式碼

參考資料

Scalable IO in Java
Netty 系列之 Netty 執行緒模型

相關文章