Multi-Threaded Synchronization
* A system needs to communicate with an [`Engine`] running in a separate thread.
* Multiple [`Engine`]s running in separate threads need to coordinate/synchronize with each other.
* An MPSC channel (or any other appropriate synchronization primitive) is used to send/receive
messages to/from an [`Engine`] running in a separate thread.
* An API is registered with the [`Engine`] that is essentially _blocking_ until synchronization is achieved.
Example
use rhai::{Engine};
fn main() {
// Channel: Script -> Master
let (tx_script, rx_master) = std::sync::mpsc::channel();
// Channel: Master -> Script
let (tx_master, rx_script) = std::sync::mpsc::channel();
// Spawn thread with Engine
std::thread::spawn(move || {
// Create Engine
let mut engine = Engine::new();
// Register API
// Notice that the API functions are blocking
engine.register_fn("get", move || rx_script.recv().unwrap())
.register_fn("put", move |v: i64| tx_script.send(v).unwrap());
// Run script
engine.run(
r#"
print("Starting script loop...");
loop {
// The following call blocks until there is data
// in the channel
let x = get();
print(`Script Read: ${x}`);
x += 1;
print(`Script Write: ${x}`);
// The following call blocks until the data
// is successfully sent to the channel
put(x);
}
"#).unwrap();
});
// This is the main processing thread
println!("Starting main loop...");
let mut value = 0_i64;
while value < 10 {
println!("Value: {}", value);
// Send value to script
tx_master.send(value).unwrap();
// Receive value from script
value = rx_master.recv().unwrap();
}
}
Considerations for sync
std::mpsc::Sender
and std::mpsc::Receiver
are not Sync
, therefore they cannot be used in
registered functions if the sync
feature is enabled.
In that situation, it is possible to wrap the Sender
and Receiver
each in a Mutex
or RwLock
,
which makes them Sync
.
This, however, incurs the additional overhead of locking and unlocking the Mutex
or RwLock
during every function call, which is technically not necessary because there are no other references
to them.
The example above highlights the fact that Rhai scripts can call any Rust function, including ones
that are _blocking_.
However, Rhai is essentially a blocking, single-threaded engine. Therefore it does _not_ provide an
async API.
That means, although it is simple to use Rhai within a multi-threading environment where blocking a
thread is acceptable or even expected, it is currently not possible to call _async_ functions within
Rhai scripts because there is no mechanism in [`Engine`] to wrap the state of the call stack inside
a future.
Fortunately an [`Engine`] is re-entrant so it can be shared among many async tasks. It is usually
possible to split a script into multiple parts to avoid having to call async functions.
Creating an [`Engine`] is also relatively cheap (extremely cheap if creating a [raw `Engine`]),
so it is also a valid pattern to spawn a new [`Engine`] instance for each task.