Multithreading
Rune is thread safe, but the Vm
does not implement Sync
so cannot directly
be shared across threads. This section details instead how you are intended to
use Rune in a multithreaded environment.
Compiling a Unit
and a RuntimeContext
are expensive operations compared
to the cost of calling a function. So you should try to do this as little as
possible. It is appropriate to recompile a script when the source of the script
changes. See the Hot reloading section for more information on this.
Once you have a Unit
and a RuntimeContext
they are thread safe and can be
used by multiple threads simultaneously through Arc<Unit>
and
Arc<RuntimeContext>
. Constructing a Vm
with these through Vm::new
is a
very cheap operation.
#![allow(unused)] fn main() { let unit: Arc<Unit> = /* todo */; let context: Arc<RuntimeContext> = /* todo */; std::thread::spawn(move || { let mut vm = Vm::new(unit, context); let value = vm.call(["function"], (42,))?; Ok(()) }); }
Virtual machines do allocate memory. To avoide this overhead you'd have to employ more advanced techniques, such as storing virtual machines in a pool or thread locals. Once a machine has been acquired the
Unit
andRuntimeContext
associated with it can be swapped out to the ones you need usingVm::unit_mut
andVm::context_mut
respectively.
Using Vm::send_execute
is a way to assert that a given execution is thread
safe. And allows you to use Rune in asynchronous multithreaded environments,
such as Tokio. This is achieved by ensuring that all captured arguments are
ConstValue
's, which in contrast to Value
's are guaranteed to be
thread-safe:
use rune::alloc::prelude::*; use rune::termcolor::{ColorChoice, StandardStream}; use rune::{Diagnostics, Vm}; use std::sync::Arc; #[tokio::main] async fn main() -> rune::support::Result<()> { let context = rune_modules::default_context()?; let runtime = Arc::new(context.runtime()?); let mut sources = rune::sources! { entry => { async fn main(timeout) { time::sleep(time::Duration::from_secs(timeout)).await } } }; let mut diagnostics = Diagnostics::new(); let result = rune::prepare(&mut sources) .with_context(&context) .with_diagnostics(&mut diagnostics) .build(); if !diagnostics.is_empty() { let mut writer = StandardStream::stderr(ColorChoice::Always); diagnostics.emit(&mut writer, &sources)?; } let unit = result?; let vm = Vm::new(runtime, Arc::new(unit)); let execution = vm.try_clone()?.send_execute(["main"], (5u32,))?; let t1 = tokio::spawn(async move { execution.async_complete().await.unwrap(); println!("timer ticked"); }); let execution = vm.try_clone()?.send_execute(["main"], (2u32,))?; let t2 = tokio::spawn(async move { execution.async_complete().await.unwrap(); println!("timer ticked"); }); tokio::try_join!(t1, t2).unwrap(); Ok(()) }
Finally Function::into_sync
exists to coerce a function into a
SyncFunction
, which is a thread-safe variant of a regular Function
. This
is a fallible operation since all values which are captured in the function-type
in case its a closure has to be coerced to ConstValue
. If this is not the
case, the conversion will fail.