Multi-Threaded Synchronization ============================== {{#include ../links.md}} ```admonish info "Usage scenarios" * 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. ``` ```admonish abstract "Key concepts" * 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 ------- ```rust 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. ```admonish note "Async" 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. ```