Multi-Threaded Synchronization
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.