Promise and future: Secrets of Gemini

Time:2020-11-27

Yongshun Daniel wrote a series of tutorials “no secret under the source code – do the best netty source code analysis tutorial” is the best netty source analysis article I have read so far. But I don’t know why. When I write the third chapter, the author stops the shift. Therefore, I would like to try to write the following chapters with my personal understanding.

Write it first

Senior Yongshun has completed the following chapters:

  • Netty source code analysis
  • Netty source code analysis zero sharpening knife cutting firewood industry source code analysis environment construction
  • Netty source code analysis Part 1: uncover the mysterious red cover of bootstrap (client)
  • Netty source code analysis Part 1: uncovering the mysterious red cover of bootstrap (server side)
  • Netty source code analysis Part 2: the artery of netty channel pipeline (1)
  • Netty source code analysis Part 2: the main artery of netty channel pipeline (2)
  • Netty source code analysis 3 I am the famous EventLoop (1)
  • Netty source code analysis 3 I am the famous EventLoop (2)

Continuation section:

  • Promise and future: Secrets of Gemini
  • Netty source code analysis 5 Pentium blood: bytebuf
  • Netty source code analysis Part 6 pipeline processor: handler

The netty version used in this article is 4.1.33

The relationship between future < V > and promise < V >

Netty’s internal io.netty.util . concurrent.Future <5> Inherited from java.util.concurrent . future < V >, while promise < V > is a special implementation of the former.
Promise and future: Secrets of Gemini

Java Native future < V >

The future < V > interface is provided under the Java concurrent programming package. Future represents the result of the asynchronous operation in asynchronous programming. The internal method of future < V > can realize the operations such as status checking, canceling execution and obtaining execution result. The internal methods are as follows:

//Attempt to cancel execution
    boolean cancel(boolean mayInterruptIfRunning);
    //Has the execution been cancelled
    boolean isCancelled();
    //Has the execution been completed
    boolean isDone();
    //Block get execution result
    V get() throws InterruptedException, ExecutionException;
    //Block get execution result或超时后返回
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;

Netty’s extension of future < V >

The function of native future < V > is limited. Netty extends future < V > and adds the following methods:

  • A richer method of state judgment is added
//Judge whether the execution is successful
    boolean isSuccess();
    //Judge whether the execution can be cancelled
    boolean isCancellable();
  • Abnormal I / O operation caused by support acquisition
    Throwable cause();
  • Added the method of monitoring callback to support the callback method specified by the user after the future is completed
//Add callback method
    Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);
    //Add multiple callback methods
    Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
    //Delete callback method
    Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);
    //Delete multiple callback methods
    Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);
  • Two kinds of methods for blocking and waiting for the result to return are added. One of them is the sync method, which blocks the waiting result and throws an exception that causes the failure if the execution fails; the other is the await method, which only blocks the return of waiting result and does not throw an exception.
//Block the wait and throw an exception if it fails
    Future<V> sync() throws InterruptedException;
    //Same as above, the difference is non interruptible blocking waiting process
    Future<V> syncUninterruptibly();

    //Blocking wait
    Future<V> await() throws InterruptedException;
    //Same as above, the difference is non interruptible blocking waiting process
    Future<V> awaitUninterruptibly();

Promise<V>

Promise < V > interface continues to inherit future < V > and adds several methods to set state and callback

//Set success status and call back
    Promise<V> setSuccess(V result);
    boolean trySuccess(V result);
    //Set failure status and callback
    Promise<V> setFailure(Throwable cause);
    boolean tryFailure(Throwable cause);
    //Set to non cancelable state
    boolean setUncancellable();

It can be seen that promise < V > as a special future < V > only adds some state setting methods. Therefore, it is often used in incoming I / O business code to set the success (or failure) status after I / O ends and call back methods.

Setting I / O execution results through promise

Taking the registration process of client connection as an example, the call link is as follows:

io.netty.bootstrap.Bootstrap.connect()
--> io.netty.bootstrap.Bootstrap.doResolveAndConnect()
---->io.netty.bootstrap.AbstractBootstrap.initAndRegister()
------>io.netty.channel.MultithreadEventLoopGroup.register()
-------->io.netty.channel.SingleThreadEventLoop.register()

Trace to the singlethreadeventloop, and you will see this Code:

    @Override
    public ChannelFuture register(Channel channel) {
        return register(new DefaultChannelPromise(channel, this));
    }

A new defaultchannelpromise is created here. The constructor passes in the current channel and the current thread this. From the class diagram in Section 1, we know that defaultchannelpromise implements both future and promise, with all the methods mentioned above.

Then continue to pass the promise into another register method:

    @Override
    public ChannelFuture register(final ChannelPromise promise) {
        ObjectUtil.checkNotNull(promise, "promise");
        promise.channel().unsafe().register(this, promise);
        return promise;
    }

In the register method, continue to pass the promise to the unsafe register method, and immediately return the promise in the form of channelfuture. Obviously, this is an asynchronous callback processing: the upper layer business can get the returned channelfuture blocking waiting result or set the callback method, while the promise that continues to pass down can be used to set the execution status and callback the method set.

As we continue to debug, we can see:

// io.netty.channel.AbstractChannel.AbstractUnsafe.java
 @Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            if (eventLoop == null) {
                throw new NullPointerException("eventLoop");
            }
            if (isRegistered()) {
                //If it has been registered, it will be set to fail
                promise.setFailure(new IllegalStateException("registered to an event loop already"));
                return;
            }
            if (!isCompatible(eventLoop)) {
                //If the thread type is incompatible, it is set to fail
                promise.setFailure(
                        new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
                return;
            }

            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    //Failed to set promise to abnormal condition
                    safeSetFailure(promise, t);
                }
            }
        }

        private void register0(ChannelPromise promise) {
            try {
                //Before registration, set promise to non cancelable transition
                if (!promise.setUncancellable() || !ensureOpen(promise)) {
                    return;
                }
                boolean firstRegistration = neverRegistered;
                doRegister();
                neverRegistered = false;
                registered = true;

                pipeline.invokeHandlerAddedIfNeeded();
                //Promise set to success
                safeSetSuccess(promise);
                pipeline.fireChannelRegistered();
             
                if (isActive()) {
                    if (firstRegistration) {
                        pipeline.fireChannelActive();
                    } else if (config().isAutoRead()) {
                        beginRead();
                    }
                }
            } catch (Throwable t) {
                // Close the channel directly to avoid FD leak.
                closeForcibly();
                closeFuture.setClosed();
                //Failed to set promise to abnormal condition
                safeSetFailure(promise, t);
            }
        }

It can be seen that whether the underlying I / O operation is successful or not can be set by promise, and the outer channel future can perceive the result of I / O operation.

Get I / O execution results through channelfuture

Let’s take a look at the purpose of the returned channel future:

// io.netty.bootstrap.AbstractBootstrap.java

    final ChannelFuture initAndRegister() {
        //...
        ChannelFuture regFuture = config().group().register(channel);
        //If the exception is not null, it means that the underlying I / O has failed, and promise has set the failure exception
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }

Here, I / O failure can be detected in advance by checking whether the failure exception stack is empty. We can also see that:

// io.netty.bootstrap.AbstractBootstrap.java

 private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        //If the registration is successful
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
       //If the registration is not completed
       // ...
    }
}

Here, you can know whether the underlying registration is completed through the channelfuture ා isdone() method. If so, continue to bind.

However, because registration is an asynchronous operation, if the registration may not be completed at this time, the following logic will be entered:

// io.netty.bootstrap.AbstractBootstrap.java

//...
else {
    // Registration future is almost always fulfilled already, but just in case it's not.
    final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
    regFuture.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            Throwable cause = future.cause();
            if (cause != null) {
                // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                // IllegalStateException once we try to access the EventLoop of the Channel.
                promise.setFailure(cause);
            } else {
                // Registration was successful, so set the correct executor to use.
                // See https://github.com/netty/netty/issues/2586
                promise.registered();

                doBind0(regFuture, channel, localAddress, promise);
            }
        }
    });
    return promise;
}

Here, a new pendingregistrationpromise is created, a callback method is added to the original channelfuture object, and the status of pendingregistrationpromise is changed in the callback, and pendingregistrationpromise will continue to be passed to the upper level. When the underlying promise state is set and called back, the callback method is entered. The I / O status continues to pass out.

The principle of result transfer of defaultchannelpromise

We’ve seen the asynchronous model of promise and future. Let’s look at how the bottom is implemented. Take the most commonly used defaultchannelpromise as an example. Its internal is very simple. We mainly look at its parent class, defaultpromise:

//Atomic updater for result field
    @SuppressWarnings("rawtypes")
    private static final AtomicReferenceFieldUpdater<DefaultPromise, Object> RESULT_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(DefaultPromise.class, Object.class, "result");
    //Fields for caching execution results
    private volatile Object result;
    //Thread where promise is located
    private final EventExecutor executor;
    //One or more callback methods
    private Object listeners;
    //Number of blocked threads counter  
    private short waiters;

Set status

Take setting success status as an example

@Override
    public Promise<V> setSuccess(V result) {
        if (setSuccess0(result)) {
            //Call the callback method
            notifyListeners();
            return this;
        }
        throw new IllegalStateException("complete already: " + this);
    }

    private boolean setSuccess0(V result) {
        return setValue0(result == null ? SUCCESS : result);
    }

    private boolean setValue0(Object objResult) {
        //Atomic modify the result field to objResult
        if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
            RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
            checkNotifyWaiters();
            return true;
        }
        return false;
    }

    private synchronized void checkNotifyWaiters() {
        if (waiters > 0) {
            //If there are other threads waiting for the result of the promise, wake them up
            notifyAll();
        }
    }

To set the state of promise is to modify the result field atomically to the incoming execution result. It is worth noting that the result field has the volatile keyword to ensure visibility between multiple threads. In addition, after setting the state, it will try to wake up all threads that are blocking waiting for the promise to return the result.

The other methods of setting the state are basically the same.

The thread is blocked to execute the result and wait

As mentioned above, other threads will block and wait for the promise to return results. The specific implementation takes the sync method as an example

@Override
    public Promise<V> sync() throws InterruptedException {
        //Blocking wait
        await();
        //If there is an exception, it is thrown
        rethrowIfFailed();
        return this;
    }

    @Override
    public Promise<V> await() throws InterruptedException {
        if (isDone()) {
            //If it's done, go back directly
            return this;
        }
        //Can be interrupted
        if (Thread.interrupted()) {
            throw new InterruptedException(toString());
        }
        //Dead circulation check
        checkDeadLock();

        synchronized (this) {
            while (!isDone()) {
                //Up counter (used to record how many threads are waiting for the promise to return results)
                incWaiters();
                try {
                    //Blocking wait结果
                    wait();
                } finally {
                    //Down counter
                    decWaiters();
                }
            }
        }
        return this;
    }

All threads that call the sync method are blocked until promise is set to success or failure. This also explains why the netty client or server usually calls the sync method when it starts. Essentially, it blocks the current thread and waits for the I / O result to return asynchronously, as follows:

Bootstrap bootstrap = new Bootstrap();
    ChannelFuture future = bootstrap.group(new NioEventLoopGroup(10))
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    //Add parser based on newline character to pipeline
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    //Adding string codec to pipeline
                    ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
                    ch.pipeline().addLast(new StringEncoder(Charset.forName("UTF-8")));
                    //Add server-side processing logic to the pipeline
                    ch.pipeline().addLast(new MyClientEchoHandler());
                }
            }).connect("127.0.0.1", 9898).sync();

    future.channel().closeFuture().sync();

Callback mechanism

@Override
    public Promise<V> addListener(GenericFutureListener<? extends Future<? super V>> listener) {
        checkNotNull(listener, "listener");

        synchronized (this) {
            //Add callback method
            addListener0(listener);
        }

        if (isDone()) {
            //If the I / O operation has ended, the callback is triggered directly
            notifyListeners();
        }

        return this;
    }

    private void addListener0(GenericFutureListener<? extends Future<? super V>> listener) {
        if (listeners == null) {
            //Only one callback method is assigned directly
            listeners = listener;
        } else if (listeners instanceof DefaultFutureListeners) {
            //Add the callback method to the listeners array maintained internally by defaultfuturelisteners
            ((DefaultFutureListeners) listeners).add(listener);
        } else {
            //If there are multiple callback methods, create a new defaultfuturelisteners to save more callback methods
            listeners = new DefaultFutureListeners((GenericFutureListener<?>) listeners, listener);
        }
    }

As can be seen from the above, after adding a callback method, it will immediately check whether promise has been completed; if promise has been completed, the callback method will be called immediately.

summary

The promise and future mechanism of netty is developed based on future < V > under Java Concurrent package. Among them, future supports blocking waiting, adding callback methods and judging execution status, while promise mainly supports state setting related methods. When the underlying I / O operation changes the execution state through promise, we can get the result immediately by synchronizing the waiting future.

Therefore, as the title of Yongshun Daniel said, promise and future are closely linked like Gemini in netty’s asynchronous model. But I think they’re more like two electrons in a quantum entanglement, because changing the state of one side makes the other party feel it immediately.

So far, the core principles of promise and future have been analyzed.