1.6.Context上下文

    “上下文”对所有开发人员而言一直是一个比较模糊的概念,让我们从”语境“开始理解这个上下文。如果您有一个单词:“该生”,它表示您所讲到的一个”学生“,那么这个学生在不同的上下文有可能语义会发生变化。

  • 老师看到小明在卖糖葫芦,叹了一口气,说:“该生家庭条件……”。

  • 我在补习班遇到了小刚,它很认真在做作业,该生……。

  • 刘老师在和一个学生打台球,该生的名字叫做……。

    明白了么?上下文提供的是一个环境的概念,同样的,Vert.x中也存在环境的概念,这里的环境就称为Context,它和Vertx实例以及Verticle实例都息息相关。既然提到了上下文,那么我们看看编程中的上下文一般表示什么?

    上下文在编程环境中是一个比较泛化的概念,大致意思就是:“和现在的对象执行相关的周围环境”,但是这个**“环境”究竟表示什么,却取决于真正的运行环境,对Vertx实例和Verticle实例而言,它的上下文您可以理解成:Vert.x工具集中提供的容器环境。在Vert.x中用于描述上下文的类为:io.vertx.core.Context^1,这个类用来描述此处提到的“上下文”**。

1. 基本概念

    在Verticle的启动/停止过程中,Vert.x提供了一个事件(Event)来调用Verticle实例中的start/stop方法,这个事件的执行会关联到一个io.vertx.core.Context实例。

    通常一个上下文实例就是一个Event Loop的Context实例,它会指定一个Event Loop线程与Verticle实例关联,所以和这个Context实例相关的所有事件的执行通常都是在相同的Event Loop线程中——但是:Vert.x中的Context是有类型概念的,当您使用的Verticle实例是一个Worker或Multi-Thread Worker时,这个时候的Context就不应该和Event Loop关联了,而是直接和Worker Pool中的线程关联。

Context context = vertx.getOrCreateContext();

    上边代码是在Vert.x中用来创建Context对象的代码,更直接点是通过”获取/创建“的方式去拿到Context引用信息,既然谈到了上下文Context和Verticle实例相关,那么由于Verticle实例运行的线程池有差异,所以可以通过下边的代码来判断当前的上下文环境是在哪个线程池中运行。

Context context = vertx.getOrCreateContext();
if (context.isEventLoopContext()) {
    System.out.println("Context attached to Event Loop");
} else if (context.isWorkerContext()) {
    System.out.println("Context attached to Worker Thread");
} else if (context.isMultiThreadedWorkerContext()) {
    System.out.println("Context attached to Worker Thread - multi threaded worker");
} else if (! Context.isOnVertxThread()) {
    System.out.println("Context not attached to a thread managed by vert.x");
}

    上述代码是官方提供出来上下文的”判断“代码,它可以帮助您判断当前的Context是在哪个线程池中运行的,从上述代码中可以知道,Context可以直接和三种线程直接关联:

  1. Event Loop线程

  2. Worker Pool线程

  3. Multi-Thread Worker Pool线程

  4. 非Vertx托管线程(这种线程在原始代码中是会直接抛出异常的,从Context not attached to a thread managed by vert.x消息就可以窥见)

2. 异步执行

    参考下边的代码,最直接的一种Context的用法就是执行异步代码:

package io.vertx.up._01.lanucher;

import io.vertx.core.Context;

public class ContextLauncher {
    public static void main(final String[] args) {
        // 哪种模式,这里使用了非Cluster模式
        final boolean isClustered = false;
        final Launcher launcher = isClustered ? new ClusterLauncher() : new SingleLauncher();
        System.out.println(Thread.currentThread().getName() + ","
                + Thread.currentThread().getId());
        launcher.start(vertx -> {
            // 上下文
            final Context context = vertx.getOrCreateContext();
            context.runOnContext(v -> {
                System.out.println(Thread.currentThread().getName() + ","
                        + Thread.currentThread().getId()
                        + ", This will be executed async -> " + v);
            });
        });
    }
}

    当您运行上述代码将会得到下边的输出:

main,1
vert.x-eventloop-thread-0,14, This will be executed async -> null

    这段代码和官方代码不同的地方是为了给读者解释执行的线程池,从输出中可以知道,Context本身是在Event Loop中运行的,也就是默认情况,所以线程名称为vert.x-eventloop-前缀格式,而启动Vertx实例的代码是在主线程中运行。这个API演示的是context中的异步代码执行过程,而Context接口这里的执行只干了一件事就是调用runOnContext执行Handler里面的内容。

3. 数据存储

    除了上述的异步执行以外,您还可以将数据存储在上下文中,需要注意的一点是这个地方存储的数据对于不同的Handler处理器而言,同一个Context中的数据是可共享的。

final Context context = vertx.getOrCreateContext();
context.put("data", "hello");
context.runOnContext((v) -> {
    String hello = context.get("data");
});

    上述代码是官方文档中的代码,最终在Handler里拿到的内容就是上边存入的hello字符串,Context中三个相关API对应的定义为:

  void put(String key, Object value);

  boolean remove(String key);

  <T> T get(String key);

4. 其他

    除了这里介绍的异步执行、数据存储(应该是这两种很高频使用,所以官方只提到了这里),Context类中还包含下边这些API可让开发者关注:

// 设置关闭的生命周期相关代码
void addCloseHook(Closeable hook);
void removeCloseHook(Closeable hook);
// 设置访问异常相关的Handler
Handler<Throwable> exceptionHandler();
Context exceptionHandler(@Nullable Handler<Throwable> handler);
// 执行非异步阻塞动作
<T> void executeBlocking(Handler<Future<T>> blockingCodeHandler, 
                         boolean ordered, Handler<AsyncResult<T>> resultHandler);
<T> void executeBlocking(Handler<Future<T>> blockingCodeHandler, 
                         Handler<AsyncResult<T>> resultHandler);

5. 源码浅析

    若在上下文部分仅提供这么少的内容,那么这些内容就和官方文档无异了,我在zero开发中由于直接使用了vertx-web项目,所以关于上下文接触并不多,另外一个小剧本,我们直接去Context这个方法的内部看看源代码,理解一下Vert.x是如何使用Context的。

getOrCreateContext

    回到最初的时刻,还是来细看Vertx实例如何去获取/创建一个上下文的,参考下图:

    上图是创建Context上下文对象的完整代码流程图——仅限于创建EventLoopContext(ContextImpl的子类),还不涉及到工作线程上下文,有几个关键的检查点需要注意(菱形部分),注意线条中的call,它表示调用,所以完整的流程如下:

  • 先使用Thread.currentThread读取当前执行线程,并且检查它是否为一个VertxThread类型的线程,该线程的父类是Netty中的io.netty.util.concurrent.FastThreadLocalThread,并不是java.lang.Thread;如果这里拿到的实例是null,那么回退到最早的分支节点。

  • getContext方法内部调用context()方法时,做了第二次检查,检查当前上下文应用的附加条件:owner引用是否是当前Vertx实例。——可以理解的是,在VertxThread去获取Context引用时,一旦拿到后,这个上下文相关的Context引用就和当前的Vertx实例关联上了,所以这里的检查会做一次“后期判断”,检查不成功同样退回到最早的分支节点。

  • 最后,如果上边两步都返回了nullContext引用,那么就调用内部的createEventLoopContext方法创建嵌入内部的一个EventLoop相关的上下文。

    上述逻辑中的最后一点可以参考代码中的注释:

if (ctx == null) {
    // We are running embedded - Create a context
    ctx = createEventLoopContext(null, null, new JsonObject(), 
        Thread.currentThread().getContextClassLoader());
}

**「注」**到现在为止,所有的代码流程中实际上并没有关系到Context的创建细节,只是单纯在Vert.x中告诉了读者getOrCreateContext这个方法的背后发生了什么,根据上边的代码流程希望读者对这个方法有一个更深入的了解,关于Context上下文引用本身的数据结构,读者可以阅读io.vertx.core.impl.ContextImpl这个类的源代码。

put/get/remove

    之所以继续分析这三个配套方法的源代码,是因为在Vert.x中上下文里存储了相关的环境数据信息,这些数据的访问接口是透过这三个API来完成的,位于io.vertx.core.impl.ContextImpl类中。这三个方法的核心代码如下:

  @Override
  @SuppressWarnings("unchecked")
  public <T> T get(String key) {
    return (T) contextData().get(key);
  }

  @Override
  public void put(String key, Object value) {
    contextData().put(key, value);
  }

  @Override
  public boolean remove(String key) {
    return contextData().remove(key) != null;
  }

  上边三个方法都调用了contextData(),而这个方法的实现及其简单:

  public synchronized ConcurrentMap<Object, Object> contextData() {
    if (contextData == null) {
      contextData = new ConcurrentHashMap<>();
    }
    return contextData;
  }

    这里就不涉及额外的讲解了,这三个方法内部居然只是单纯的ConcurrentHashMap,那么这三个方法的具体实现就非常清晰了。

executeBlocking

    有意思的剧情来了,也许对大多数Vert.x的爱好者而言,最想知道的就是它的非阻塞、纯异步的实现,而在目前的代码中,我们看到了一个阻塞方法executeBlocking,是的,这个方法让您可以在Context上下文中去执行一段阻塞式的代码,看过官方文档的朋友都不会陌生这个方法vertx.executeBlocking^2,从VertxImpl的实现可以看到,这个方法背后的逻辑就是我们将要讨论的ContextImpl实例中的executeBlocking方法。

  <T> void executeBlocking(Action<T> action, Handler<Future<T>> blockingCodeHandler,
      Handler<AsyncResult<T>> resultHandler,
      Executor exec, TaskQueue queue, PoolMetrics metrics) {
    Object queueMetric = metrics != null ? metrics.submitted() : null;
    try {
      Runnable command = () -> {
        VertxThread current = (VertxThread) Thread.currentThread();
        Object execMetric = null;
        if (metrics != null) {
          execMetric = metrics.begin(queueMetric);
        }
        if (!DISABLE_TIMINGS) {
          current.executeStart();
        }
        Future<T> res = Future.future();
        try {
          if (blockingCodeHandler != null) {
            ContextImpl.setContext(this);
            blockingCodeHandler.handle(res);
          } else {
            T result = action.perform();
            res.complete(result);
          }
        } catch (Throwable e) {
          res.fail(e);
        } finally {
          if (!DISABLE_TIMINGS) {
            current.executeEnd();
          }
        }
        if (metrics != null) {
          metrics.end(execMetric, res.succeeded());
        }
        if (resultHandler != null) {
          runOnContext(v -> res.setHandler(resultHandler));
        }
      };
      if (queue != null) {
        queue.execute(command, exec);
      } else {
        exec.execute(command);
      }
    } catch (RejectedExecutionException e) {
      // Pool is already shut down
      if (metrics != null) {
        metrics.rejected(queueMetric);
      }
      throw e;
    }
  }

    看到上边的源代码,可能很多读者就会觉得又枯燥了,还是使用最简单的图示法:

    上述流程图只是这个方法的流程图,关于新线程Runnable的逻辑细节这里就不详细阐述了,读者可以自己去品读,图的目的只是给读者一个可读代码的“心情”而已,也是俯瞰这份代码的一个视觉,所以读者可以根据代码流程图去细细品读上述源码。

6.小结

    因为zero开发过程中直接使用了vertx-web项目,所以真正接触Context的机会并不多,但还是拼拼凑凑地写了这么多,趣味不趣味读者自己可以去体会,那么可以把这一章的内容当做逐陆记中的休息区,大家可以稍稍喘口气。况且在官方文档中并没有提到Context作为Vert.x中的核心概念,所以也容易被忽略,但了解Vert.x的上下文对读者而言应该是有利的,而可以可以换个视觉看看Vert.x,何乐而不为呢?既然这个章节是初探,那么暂时点到为止,我们在后续的章节中会针对这部分内容进行更加深入的理解。

最后更新于