Embedding › 05 Concurrency
Run language code on host threads, choose between one shared VM and several isolated ones, and follow the lifecycle rules that keep the GC happy.
| One VM, many threads | Many VMs | |
|---|---|---|
| Heap | shared | one per VM |
| Singletons | shared (the same instance everywhere) | one instance per VM |
| GC | one collector, stops all threads | independent collectors |
| Use for | components that cooperate on the same data | components that must not interfere |
Both shapes can coexist: an Engine can back one shared VM and several isolated ones.
Each host thread attaches to the VM, runs its entry, and leaves:
typedef struct { VM* vm; ClassMeta* entry; int rc; } Job;
static int run_entry(void* p) {
Job* j = (Job*)p;
LothThread* t = loth_engine_thread_attach(j->vm);
if (!t) { j->rc = 2; return 2; }
j->rc = vm_run(j->vm, j->entry, 0, NULL);
loth_engine_thread_leave(j->vm, t);
return 0;
}
/* in main, after loading AppA and AppB: */
Job ja = { vm, loth_engine_entry(e, "com.example.host.AppA"), -1 };
Job jb = { vm, loth_engine_entry(e, "com.example.host.AppB"), -1 };
void* ta = loth_thread_create(run_entry, &ja, "AppA");
void* tb = loth_thread_create(run_entry, &jb, "AppB");
loth_thread_join((LothThread*)ta);
loth_thread_join((LothThread*)tb);
The example verifies the result: AppA bumps a shared singleton
2000 times, AppB 3000 times, and the host checks that the total is
exactly 5000. If each thread had its own singleton, or the increments were not
synchronized, that check would fail.
static int worker(void* p) {
Job* j = p;
Arena a;
arena_init(&a);
VM* vm = vm_new(&a);
loth_engine_setup_vm(j->e, vm);
int rc = vm_run(vm, j->entry, 0, NULL);
vm_destroy(vm);
arena_free_all(&a);
return rc;
}
Each thread owns its VM and its arena, so the threads never touch each other's heap. The Engine's modules are read-only and shared safely. Each VM can also get its own output callback to keep the output of different VMs separate.
loth_engine_thread_attach) and leave when done. The main
thread is already attached by vm_new; do not attach it
again.Value you hold across an allocation must be rooted with
vm_push_root/vm_pop_root or registered once with
vm_add_root. The GC may run on another thread at any time.vm_destroy.
Destroying a VM under running threads is undefined behavior.
vm_live_threads(vm) reports how many are still alive (the CLI
uses it to skip destruction and simply exit).vm_set_max_steps so a runaway script cannot spin forever.Sharing a heap means sharing the responsibility: unsynchronized access to the
same mutable data from two threads is undefined behavior, exactly as in the
language track. The synchronization tools (synchronized,
volatile, Lock, Cond) are the same ones
the language uses; hosts coordinate with them through language methods.