Multithreaded runtime

In all examples so far we've used the local runtime, which is limited to a single thread. The multithreaded runtime can be used from any thread, this feature requires using at least Julia 1.9 and enabling the multi-rt feature.

Using the multithreaded runtime instead of the local runtime is mostly a matter of starting the runtime differently.

use std::thread;

use jlrs::{prelude::*, runtime::builder::Builder};

fn main() {
    let (mut mt_handle, thread_handle) = Builder::new().spawn_mt().expect("cannot init Julia");
    let mut mt_handle2 = mt_handle.clone();

    let t1 = thread::spawn(move || {
        mt_handle.with(|handle| {
            handle.local_scope::<_, 1>(|mut frame| {
                // Safety: we're just printing a string
                unsafe { Value::eval_string(&mut frame, "println(\"Hello from thread 1\")") }
                    .expect("caught exception");
            })
        })
    });

    let t2 = thread::spawn(move || {
        mt_handle2.with(|handle| {
            handle.local_scope::<_, 1>(|mut frame| {
                // Safety: we're just printing a string
                unsafe { Value::eval_string(&mut frame, "println(\"Hello from thread 2\")") }
                    .expect("caught exception");
            })
        })
    });

    t1.join().expect("thread 1 panicked");
    t2.join().expect("thread 2 panicked");
    thread_handle.join().expect("runtime thread panicked")
}

Julia is initialized on a background thread when spawn_mt is called. This method returns an MtHandle that we can use to call into Julia and a handle to the runtime thread. The MtHandle can be cloned and sent to other threads, by calling MtHandle::with the thread is temporarily put into a state where it can create scopes and call into Julia. The runtime thread shuts down when all MtHandles have been dropped.

Instead of spawning the runtime thread, we can also initialize Julia on the current thread and spawn a new thread that can use an MtHandle:

use std::thread;

use jlrs::{
    prelude::*,
    runtime::{builder::Builder, handle::mt_handle::MtHandle},
};

fn main_inner(mut mt_handle: MtHandle) {
    let mut mt_handle2 = mt_handle.clone();

    let t1 = thread::spawn(move || {
        mt_handle.with(|handle| {
            handle.local_scope::<_, 1>(|mut frame| {
                // Safety: we're just printing a string
                unsafe { Value::eval_string(&mut frame, "println(\"Hello from thread 1\")") }
                    .expect("caught exception");
            })
        })
    });

    let t2 = thread::spawn(move || {
        mt_handle2.with(|handle| {
            handle.local_scope::<_, 1>(|mut frame| {
                // Safety: we're just printing a string
                unsafe { Value::eval_string(&mut frame, "println(\"Hello from thread 2\")") }
                    .expect("caught exception");
            })
        })
    });

    t1.join().expect("thread 1 panicked");
    t2.join().expect("thread 2 panicked");
}

fn main() {
    Builder::new().start_mt(main_inner).expect("cannot init Julia");
}

This is useful if we interact with code in Julia that is picky about being called from the main application thread, e.g. code involving Qt.