The basic model
The model is based on the following three simple rules:
- In a polyglot application, an arbitrary number of JS runtimes can be created, but they should be used by one thread at a time.
GraalVM enforces these rules at runtime, therefore making it easier and safer to reason about parallel and concurrent execution in a polyglot application.
Embedding Graal.js in a Java multi-threaded application
According to our simple rules, concurrent access to a shared JS object is not allowed in GraalVM. In fact, executing the following code:
will result in the following exception:
java.lang.IllegalStateException: Multi-threaded access requested by thread Thread[main,0,main] but is not allowed for language(s) js!
as expected, in both examples GraalVM enforced rule #3: no concurrent access to JS objects can happen. This, however, does not mean that concurrent threads cannot execute JS code in parallel. In fact, what really matters is that concurrent threads do not access the same runtime concurrently. Intuitively, there are two alternative ways to achieve parallel execution without violating rule #3:
- Using multiple, isolated, JS runtimes,
- Sharing a single JS runtime between threads, using proper Java synchronization to prevent concurrent access.
Depending on the embedding scenario, an approach might be more appropriate than the other one. Using multiple runtimes is straightforward:
Sharing of a single context can also be implemented quite easily using Java synchronization:
Sharing Java objects between contexts
A Java object can be shared between independent JS contexts via GraalVM’s polyglot bindings, in the following way:
counteris a Java
AtomicIntegerobject, it can be accessed by multiple threads, concurrently, and no exception will be thrown.
Multi-threaded interop in Node.js
Context does not happen via the Context API, but happens implicitly together with the creation of a new Node.js event loop. Similarly, there is no direct way to create a new Java thread, and GraalVM must rely on the built-in threading support of Node.js.
Context is created for every new worker. Node.js workers are a great way to offload CPU-intensive computation to another thread, without having to block the main Node.js event loop. Beyond “traditional” Node.js applications, Workers can be pretty convenient in polyglot applications, too. Here is an example of a Node.js application that uses Workers to perform a method call using GraalVM’s Java interoperability:
Sharing Java objects between workers
Like with Java applications and threads, sharing of Java objects between multiple workers is allowed in Node.js with GraalVM. This can be achieved using the standard built-in
postMessage() API of Node.js’ workers. Here is an example of a parallel Node.js application that receives a Java random number generator from the main Node.js event loop, uses it to create a new Java object (an awt Point), which is then returned back to the main Node.js worker thread:
One of the nice advantages of being able to share Java objects between workers, is that Java objects can be used to achieve synchronization between the main Node.js event loop and Java code. For example, Java synchronization can be used to build a notification API to coordinate Java and the Node.js’ event loop. Here is a simple Node.js application that uses a (shared) Java concurrent queue to implement a notification API, which could be used to notify Node.js when something happens in Java space (e.g., a new request arrives, some long-running computation completes, etc.):
An implementation of an
AwesomeClass could use the queue in the following way:
Quite obviously, this is a very naive implementation. More complex implementations could be developed and conveniently packed into npm modules, to be then re-used by other GraalVM applications.