2.3 当代码需要阻塞时
在事件循环上运行代码的基本规则是它不应该阻塞,它应该运行“足够快”。 您之前已经看到,默认情况下,Vert.x 会在事件循环被阻塞时间过长时检测并发出警告。
在某些情况下,您将很难避免阻塞代码。 这可能是因为您正在使用具有其他线程模型的第三方库,例如某些网络服务的驱动程序。 Vert.x 提供了两个选项来处理这种情况:worker verticles 和 executeBlocking
操作。
2.3.1 Worker verticles
Worker Verticle 是一种特殊形式的 Verticle,它不在事件循环上执行。 相反,它们在工作线程上执行,即从特殊工作池中获取的线程。 您可以定义自己的工作线程池并将工作线程部署到它们,但在大多数情况下,使用默认的 Vert.x 工作线程池就可以了。
一个worker verticle处理事件就像一个event-loop verticle,除了它可能需要任意长的时间来处理。 了解两点很重要:
- 一个worker verticle不依赖于一个单独的worker thread,因此与event-loop verticle不同的是,连续的事件可能不会在同一个线程上执行。
- Worker Verticle 只能在给定时间由某个工作线程访问。
💡提示: 简单来说,就像event-loop verticles一样,worker verticles也是单线程的,但与event-loop verticles不同,线程可能并不总是相同的。
package chapter2.worker;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WorkerVerticle extends AbstractVerticle{
private final Logger logger = LoggerFactory.getLogger(WorkerVerticle.class);
@Override
public void start() {
vertx.setPeriodic(10_000, id -> {
try {
logger.info("Zzz...");
Thread.sleep(8000); // <1>
logger.info("Up!");
} catch (InterruptedException e) {
logger.error("Woops", e);
}
});
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
DeploymentOptions opts = new DeploymentOptions()
.setInstances(2)
.setWorker(true); // <2>
vertx.deployVerticle("chapter2.worker.WorkerVerticle", opts);
}
}
<1>: 我们可以阻塞并且不会收到警告!
<2>: 制作 worker verticle 是一个部署选项标志。
清单2.14 显示了一个使用两个实例部署worker verticle对象的示例。每10秒,代码就会阻塞8秒。运行这个示例将产生与清单2.15 类似的输出。如您所见,不同的工作线程被用于连续的事件。
☢警告: 在部署 verticle 时,有一个为 worker verticles 启用多线程的选项,它允许一个 verticle 并发处理多个事件,打破了单线程处理的假设。 这一直被认为是相当高级的用法,许多用户最终以错误的方式使用它并发现并发错误。 该功能不再公开记录,甚至可能在未来的 Vert.x 版本中消失。 鼓励用户简单地调整工作池大小以匹配工作负载,而不是启用工作器程多线程。
2.3.2 执行阻塞操作
Worker Verticle 是运行阻塞任务的明智选择,但将阻塞代码提取到 Worker Verticle 中可能并不总是有意义。 这样做会导致执行小任务的Worker Verticle 类的数量激增,并且每个类可能无法形成一个合理的独立功能单元。
运行阻塞代码的另一个选项是使用 Vert.x 类中的 executeBlocking
方法。 该方法需要一些阻塞代码来执行,将其卸载到工作线程,并将结果作为新事件发送回事件循环,如图 2.4 所示。
以下清单显示了一个示例用法。
package chapter2.execblocking;
import io.vertx.core.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Offload extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(Offload.class);
@Override
public void start() {
vertx.setPeriodic(5000, id -> {
logger.info("Tick");
vertx.executeBlocking(this::blockingCode, this::resultHandler); // <1>
});
}
private void blockingCode(Promise<String> promise) { // <2>
logger.info("Blocking code running");
try {
Thread.sleep(4000);
logger.info("Done!");
promise.complete("Ok!"); // <3>
} catch (InterruptedException e) {
promise.fail(e);
}
}
private void resultHandler(AsyncResult<String> ar) { // <4>
if (ar.succeeded()) {
logger.info("Blocking code result: {}", ar.result());
} else {
logger.error("Woops", ar.cause());
}
}
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new Offload());
}
}
<1>: executeBlocking接受两个参数:要运行的代码和运行后的回调。
<2>: 阻塞代码接受任何类型的Promise对象。它用于最终传递结果。
<3>: Promise对象需要完成或失败,标志着阻塞代码执行的结束。
<4>: 在事件循环上处理的结果只是另一个异步结果。
下面的清单显示了运行清单2.16 中的代码的一些示例输出。正如您所看到的,执行被卸载到工作线程,但结果处理仍然发生在事件循环中。
💡提示: 默认情况下,连续的
executeBlocking
操作的结果处理顺序与调用executeBlocking
的顺序相同。executeBlocking
有一个带有附加boolean
参数的变体,当它设置为false
时,无论executeBlocking
的顺序如何,只要它们可用,结果都会作为事件循环事件在其可用时立即被提供。JavaDoc中是这样解释的: “ordered - 如果为true,那么如果
executeBlocking
在同一上下文上被多次调用,那么该上下文的执行将被串行执行,而不是并行执行。如果为false,则它们将是无顺序保证.”译者注: 也就是说如果
ordered
是false,就可以并发执行executeBlocking
了!但是不保证先后顺序.