写点什么

GraalVM 系列(三):GraalJS 多线程实践

用户头像
孤岛旭日
关注
发布于: 2021 年 05 月 19 日

在 GraalVM 系列(二):GraalVM 核心特性实践 一文中介绍了 GraalVM 的几个核心特性。本文接着前文遗留的 JS 多线程调用问题展开讨论。

通过前文的测试我们可以看到 GraalVM 下不支持对 JS 的并发执行,具体而言引用官网的描述如下:

GraalVM 支持的多线程执行的基本模型是"无共享(share-nothing)"模型,任何 JavaScript 开发人员都应该熟悉。

  1. 可以创建任意数量的 JavaScript Contexts,但每次只能由一个线程使用

  2. 不允许并发访问 JavaScript 对象:任何 JavaScript 对象不能同时被一个以上的线程访问

  3. 允许并发访问 Java 对象:任何 Java 对象都可以被任何 Java 或 JavaScript 线程同时访问

  4. 一个 JavaScript 上下文不能被两个或两个以上的线程同时访问,但可以使用适当的同步从多个线程访问同一个上下文,以确保不会发生并发访问


— https://www.graalvm.org/reference-manual/js/Multithreading/

这个限制的本意是不打破 JS 在前端开发中单线程的模型以降低适配学习成本,但这也大大限制了其使用的场景。

怎么解决呢?我们查阅 https://github.com/oracle/graal/issues/2484 这个 Issue,结合官网的介绍可以总结出两种方案。

使用 Worker

这一方案的核心是通过 Node 的 worker_threads 模块启动一个 Worker,在 Worker 中引用 Java 的阻塞队列(如:LinkedBlockingDeque)并等待数据(take),获取数据后发送(postMessage)给主线程处理,这样一来 Worker 线程阻塞用于接收,主线程非阻塞用于处理,在需要处理时从 Java 侧发送(offer)数据到阻塞队列即可。

但这一方案由于要启动 Worker,所以必须使用 Node 工程启动,不能嵌入到 Java 工程中。示例代码可参考 https://medium.com/graalvm/multi-threaded-java-javascript-language-interoperability-in-graalvm-2f19c1f9c37b 。

使用 Event Loop

这里有个示例工程实现了基于事件的并发访问: https://github.com/iitsoftware/graaljs-concurrency-problem 笔者未测试过。

本文给出使用 Vertx EventBus 的并发示例,示例工程见前文,完整代码见/multithreading 目录。

PolyglotExchanger.java


/** * 与JS的交互类. * * @author gudaoxuri */public class PolyglotExchanger {
// 创建Vertx实例 private static Vertx vertx = Vertx.vertx(); // 创建EventBus实例 private static EventBus eventBus = vertx.eventBus();
/** * 由Java侧发起JS调用请求. * * @param funName 函数名 * @param args 函数参数 * @param promise 执行回调 */ public static void request(String funName, List<Object> args, Promise<Object> promise) { // 向JS中发起地址为"__js_invoke__"的事件 eventBus.request("__js_invoke__", new JsonObject().put("funName", funName).put("args", args).toString(), (Handler<AsyncResult<Message<String>>>) event -> { // 执行返回处理 if (event.failed()) { promise.fail(event.cause()); } else { promise.complete(event.result().body()); } }); }
/** * 由JS侧调用事件订阅. * * @param processFun 订阅处理函数 */ public static void consumer(Consumer<Message<String>> processFun) { eventBus.consumer("__js_invoke__", processFun::accept); }
/** * 模拟JS调用Java发起HTTP请求. * * @param httpMethod HTTP方法 * @param url URL * @param body Body * @param header Header * @param fun 回调函数 */ public static void http(String httpMethod, String url, String body, Map<String, String> header, Consumer<String> fun) { // 模拟调用,这里仅返回请求的URL vertx.setTimer(1000, i -> fun.accept("<div>Hello:" + url + "</div>")); }
}
复制代码

task.js

// 引用PolyglotExchanger类const polyglotExchanger = Java.type('idealworld.train.graalvm.multithreading.PolyglotExchanger')
// 订阅事件polyglotExchanger.consumer(event => { // 获取到订阅数据 let data = JSON.parse(event.body()) let funName = data.funName let args = data.args // 此处可以实现要处理的逻辑 // 这里使用http调用逻辑为示例 polyglotExchanger.http('GET', 'http://127.0.0.1/s?fun=' + funName + '&args=' + args, null, null, resp => { // 处理完成后执行回调函数返回结果,这里返回的是http调用的结果 event.reply(resp) })})
复制代码

MultithreadingExample.java

/** * 多线程示例. * * @author gudaoxuri */public class MultithreadingExample {
private static final String TASK_JS = new BufferedReader(new InputStreamReader(MultithreadingExample.class.getResourceAsStream("/task.js"))) .lines().collect(Collectors.joining("\n"));
public static void main(String[] args) throws InterruptedException { var context = Context.newBuilder() .allowAllAccess(true) // 开启Java函数过滤以保障安全 .allowHostClassLookup(s -> s.equalsIgnoreCase(PolyglotExchanger.class.getName())) .build(); // 添加与Java交互的函数类 context.eval(Source.create("js", TASK_JS));
// 执行测试 new Thread(() -> { while (true) { var promise = Promise.promise(); PolyglotExchanger.request("fun1", new ArrayList<>(), promise); promise.future() .onSuccess(resp -> System.out.println("result : " + resp)) .onFailure(e -> System.err.println("result : " + e.getMessage())); } }).start(); new Thread(() -> { while (true) { var promise = Promise.promise(); PolyglotExchanger.request("fun2", new ArrayList<>(), promise); promise.future() .onSuccess(resp -> System.out.println("result : " + resp)) .onFailure(e -> System.err.println("result : " + e.getMessage())); } }).start();
new CountDownLatch(1).await(); }
}
复制代码

执行后会输出如下信息:

……result : <div>Hello:http://127.0.0.1/s?fun=fun2&args=</div>result : <div>Hello:http://127.0.0.1/s?fun=fun2&args=</div>result : <div>Hello:http://127.0.0.1/s?fun=fun1&args=</div>result : <div>Hello:http://127.0.0.1/s?fun=fun2&args=</div>result : <div>Hello:http://127.0.0.1/s?fun=fun1&args=</div>result : <div>Hello:http://127.0.0.1/s?fun=fun2&args=</div>……
复制代码

这样我们就很方便地实现了对 JS 的并发访问。

关注我的公众号:

GraalVM系列(三):GraalJS多线程实践​mp.weixin.qq.com


发布于: 2021 年 05 月 19 日阅读数: 9
用户头像

孤岛旭日

关注

努力成为一个好爸爸、好丈夫。 2018.10.30 加入

10年前,毕业时,芳华正茂,立志Coding完美世界,10年后,35,志向未成但理想依旧,勉励之、践行之。

评论

发布
暂无评论
GraalVM系列(三):GraalJS多线程实践