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 and RuntimeContext associated with it can be swapped out to the ones you need using Vm::unit_mut and Vm::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.