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 enabling the multi-rt feature.

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

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

fn main() {
    Builder::new().start_mt(|mt_handle| {
        let t1 = mt_handle.spawn(move |mut mt_handle| {
            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 = mt_handle.spawn(move |mut mt_handle| {
            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 2\")") }
                        .expect("caught exception");
                })
            })
        });

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

Julia is initialized on the current thread when start_mt is called, the closure is called on a new thread. This method returns an MtHandle that we can use to call into Julia. The MtHandle can be cloned, we can create new scoped threads with MtHandle::spawn, and 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.