Explanation of Reactor model

1, What is the Reactor model

         Reactor pattern is a method to process concurrent service requests and submit the requests to one or more servers
An event design pattern for multiple service handlers. When the client requests arrive, the service handler uses the multi-channel allocation strategy. A non blocking thread receives all the requests, and then sends them to the relevant worker threads for processing.

Let's first look at what reactor is from the wiki:

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

From the above text, we can see the following key points:

  1. event handling
  2. one or more inputs can be processed
  3. The input events (events) are synchronously multiplexed and distributed to the corresponding request handlers (multiple) for processing through the Service Handler

From the introduction to Reactor Pattern in POSA2, we learned about the processing method of Reactor:

  1. Synchronous wait for multiple event sources to arrive (implemented by select())
  2. Demultiplex events and allocate corresponding event services for processing. This dispatch adopts server centralized processing (dispatch)
  3. The decomposed event and the corresponding event service application are separated from the dispatch service (handler)

2, Why use Reactor

         In common network services, if each client maintains a connection with the login server. Then, the server will maintain multiple connections with the client to connect, read and write with the client. Especially for long link services, the same IO connection needs to be maintained at the s-end. This is a big overhead for the server.

3, Composition of Reactor

First, we define the following three roles based on the Reactor Pattern processing mode:

  • Reactor   Dispatch I/O events to the corresponding Handler
  • Acceptor   Process new client connections and dispatch requests to the processor chain
  • Handlers   Perform non blocking read / write tasks

This is the most basic single Reactor single thread model. Among them, the Reactor thread is responsible for demultiplexing sockets. After a new connection arrives and triggers the connect event, it is handed over to the Acceptor for processing, and after an IO read-write event, it is handed over to hanlder for processing.

The main task of the Acceptor is to build a handler. After obtaining the SocketChannel related to the client, it is bound to the corresponding handler. After the corresponding SocketChannel has read-write events, it can be processed by the handler based on racotor distribution (all IO events are bound to the selector and distributed by the Reactor).

The model is suitable for the scenario where the business processing components in the processor chain can be completed quickly. However, this single thread model can not make full use of multi-core resources, so it is not used much in practice.

4, Development and types of Reactor model

four point one   Single Reactor single thread model (also known as single thread mode)

         This is the simplest single Reactor single thread model. The Reactor thread is a generalist, responsible for demultiplexing sockets, accepting new connections, and dispatching requests to the Handler processor.  

The following figure comes from "Scalable IO in Java", which is similar to the above figure. Reactor and handler are executed in one thread.

By the way, the acceptor in the figure above can be regarded as a special handler.

4.1.1   Reference code for single threaded Reactor

"Scalable IO in Java" implements a single threaded Reactor reference code. The Reactor code is as follows:

package com.crazymakercircle.ReactorModel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

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

    Reactor(int port) throws IOException
    { //Reactor initialization
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        //Non blocking
        serverSocket.configureBlocking(false);

        //Step by step processing. The first step is to receive the accept event
        SelectionKey sk =
                serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        //attach callback object, Acceptor
        sk.attach(new Acceptor());
    }

    public void run()
    {
        try
        {
            while (!Thread.interrupted())
            {
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();
                while (it.hasNext())
                {
                    //Reactor is responsible for the events received by the dispatch
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException ex)
        { /* ... */ }
    }

    void dispatch(SelectionKey k)
    {
        Runnable r = (Runnable) (k.attachment());
        //Call the previously registered callback object
        if (r != null)
        {
            r.run();
        }
    }

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

The Handler code is as follows:

package com.crazymakercircle.ReactorModel;


import com.crazymakercircle.config.SystemConfig;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;

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

    Handler(Selector selector, SocketChannel c) throws IOException
    {
        channel = c;
        c.configureBlocking(false);
        // Optionally try first read now
        sk = channel.register(selector, 0);

        //Using Handler as a callback object
        sk.attach(this);

        //Step 2: register the Read ready event
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    boolean inputIsComplete()
    {
        /* ... */
        return false;
    }

    boolean outputIsComplete()
    {

        /* ... */
        return false;
    }

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

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

    void read() throws IOException
    {
        channel.read(input);
        if (inputIsComplete())
        {

            process();

            state = SENDING;
            // Normally also do first write now

            //Step 3: receive the write ready event
            sk.interestOps(SelectionKey.OP_WRITE);
        }
    }

    void send() throws IOException
    {
        channel.write(output);

        //After writing, close the select key
        if (outputIsComplete())
        {
            sk.cancel();
        }
    }
}

These two pieces of code are based on JAVA NIO. It is recommended to understand these two pieces of code. You can see the source code in the IDE, which makes you feel better.  

4.1.2 disadvantages of single thread mode

  1. When one of the handlers is blocked, the handlers of all other clients will not be executed, and more seriously, the blocking of the handler will also cause the whole service to not receive new client requests (because the acceptor is also blocked). Because there are so many defects, the single threaded Reactor model is less used. This single thread model can not make full use of multi-core resources, so it is not used much in practice.
  2. Therefore, the single thread model is only applicable to the scenario where the business processing component in the handler can complete quickly.

four point two   Single Reactor multithreading model (also known as multithreading mode)

4.2.1   Improvement based on thread pool

Based on the thread Reactor mode, the following improvements are made:

  1. Put the execution of the Handler processor into the thread pool for multi-threaded business processing.
  2. For Reactor, it can still be a single thread. If the server is a multi-core CPU, in order to make full use of system resources, the Reactor can be divided into two threads.

A simple diagram is as follows:

4.2.2   Improved complete schematic diagram

The following figure comes from "Scalable IO in Java", which is similar to the above figure, but more detailed. Reactor is an independent thread, and the handler is executed in the thread pool.

4.2.3   Multithreading mode reference code

The reference code of "Scalable IO in Java" multithreaded Reactor is an improvement of a thread pool based on a single thread. The code of the improved Handler is as follows:

package com.crazymakercircle.ReactorModel;


import com.crazymakercircle.config.SystemConfig;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MthreadHandler implements Runnable
{
    final SocketChannel channel;
    final SelectionKey selectionKey;
    ByteBuffer input = ByteBuffer.allocate(SystemConfig.INPUT_SIZE);
    ByteBuffer output = ByteBuffer.allocate(SystemConfig.SEND_SIZE);
    static final int READING = 0, SENDING = 1;
    int state = READING;


    ExecutorService pool = Executors.newFixedThreadPool(2);
    static final int PROCESSING = 3;

    MthreadHandler(Selector selector, SocketChannel c) throws IOException
    {
        channel = c;
        c.configureBlocking(false);
        // Optionally try first read now
        selectionKey = channel.register(selector, 0);

        //Using Handler as a callback object
        selectionKey.attach(this);

        //Step 2: register the Read ready event
        selectionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    boolean inputIsComplete()
    {
       /* ... */
        return false;
    }

    boolean outputIsComplete()
    {

       /* ... */
        return false;
    }

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

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


    synchronized void read() throws IOException
    {
        // ...
        channel.read(input);
        if (inputIsComplete())
        {
            state = PROCESSING;
            //Asynchronous execution using thread pool
            pool.execute(new Processer());
        }
    }

    void send() throws IOException
    {
        channel.write(output);

        //After writing, close the select key
        if (outputIsComplete())
        {
            selectionKey.cancel();
        }
    }

    synchronized void processAndHandOff()
    {
        process();
        state = SENDING;
        // or rebind attachment
        //After the process is completed, start waiting for the write to be ready
        selectionKey.interestOps(SelectionKey.OP_WRITE);
    }

    class Processer implements Runnable
    {
        public void run()
        {
            processAndHandOff();
        }
    }

}

4.3 multi Reactor multithreading model (also known as master-slave multithreading mode)

Compared with the second model, the third model divides the Reactor into two parts,

  1. mainReactor is responsible for listening to the server socket, handling the establishment of new connections, and registering the established socketChannel with subReactor.
  2. subReactor maintains its own selector, multiplexes IO read-write events based on the socketChannel registered by mainReactor, reads and writes network data, and processes business. In addition, it is thrown to the worker thread pool to complete.

In the third model, we can see that mainReactor is mainly used to handle the establishment of network IO connection, which can usually be handled by one thread, while subReactor is mainly used for data interaction and event business processing with the established socket. Its number is generally the same as the number of CPU s, and each subReactor is handled by one county.

In this model, the work of each module is more specific, the coupling degree is lower, the performance and stability are greatly improved, and the number of concurrent clients supported can reach millions.

As for the application of this model, many excellent mine construction have been applied, such as mina and netty. The third variant of thread pool is removed from the above, which is also the default mode of Netty NIO. In the next section, we will focus on the architecture pattern of netty.

4.3.1   Master slave multithreading mode reference code

For machines with multiple CPU s, in order to make full use of system resources, the Reactor is divided into two parts. The code is as follows:

package com.crazymakercircle.ReactorModel;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

class MthreadReactor implements Runnable
{

    //Set of subReactors. A selector represents a subReactor
    Selector[] selectors=new Selector[2];
    int next = 0;
    final ServerSocketChannel serverSocket;

    MthreadReactor(int port) throws IOException
    { //Reactor initialization
        selectors[0]=Selector.open();
        selectors[1]= Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        //Non blocking
        serverSocket.configureBlocking(false);


        //Step by step processing. The first step is to receive the accept event
        SelectionKey sk =
                serverSocket.register( selectors[0], SelectionKey.OP_ACCEPT);
        //attach callback object, Acceptor
        sk.attach(new Acceptor());
    }

    public void run()
    {
        try
        {
            while (!Thread.interrupted())
            {
                for (int i = 0; i <2 ; i++)
                {
                    selectors[i].select();
                    Set selected =  selectors[i].selectedKeys();
                    Iterator it = selected.iterator();
                    while (it.hasNext())
                    {
                        //Reactor is responsible for the events received by the dispatch
                        dispatch((SelectionKey) (it.next()));
                    }
                    selected.clear();
                }

            }
        } catch (IOException ex)
        { /* ... */ }
    }

    void dispatch(SelectionKey k)
    {
        Runnable r = (Runnable) (k.attachment());
        //Call the previously registered callback object
        if (r != null)
        {
            r.run();
        }
    }


    class Acceptor { // ...
        public synchronized void run() throws IOException
        {
            SocketChannel connection =
                    serverSocket.accept(); //The main selector is responsible for accept ing
            if (connection != null)
            {
                new Handler(selectors[next], connection); //Select a subReactor to be responsible for the received connection
            }
            if (++next == selectors.length) next = 0;
        }
    }
}

4.3.2   Advantages and disadvantages of master-slave multithreading mode

advantage

  1. The response is fast and does not have to be blocked by a single synchronization time, although the Reactor itself is still synchronized;
  2. Programming is relatively simple, which can avoid complex multithreading and synchronization problems to the greatest extent, and avoid the switching overhead of multithreading / process;
  3. Scalability, which can easily make full use of CPU resources by increasing the number of Reactor instances;
  4. Reusability: reactor framework itself is independent of specific event processing logic and has high reusability;

shortcoming

  1. Compared with the traditional simple model, Reactor increases a certain complexity, so it has a certain threshold and is not easy to debug.
  2. The Reactor mode requires the underlying Synchronous Event Demultiplexer support, such as the Selector support in Java and the select system call support of the operating system. If you want to implement the Synchronous Event Demultiplexer yourself, it may not be so efficient.
  3. The Reactor mode is implemented in the same thread when I / O reads and writes data. Even if multiple Reactor mechanisms are used, if the channels sharing one Reactor have a long time of data reading and writing, it will affect the corresponding time of other channels in the Reactor. For example, in the case of large file transmission, the IO operation will affect the corresponding time of other clients, Therefore, it may be a better choice to use the traditional thread per connection for this operation, or use an improved version of Reactor mode, such as Proactor mode.

Tags: Netty Concurrent Programming NIO reactor

Posted on Tue, 14 Sep 2021 00:58:09 -0400 by woobarb