A C library that several threads call is thread-safe only as far as its own code makes it so, and its manual says how far, routine by routine. A Mica library is no different, with one difference in who does the work: the library states its shared state, and the compiler keeps it and writes the manual’s words into the header for you. The example ThreadsCallMica is a plain C program whose four POSIX threads call one Mica library at the same time. The C side calls the library’s body entry once, as every C program does, and its threads call nothing first and take no lock.
This page builds on C calls Mica, which shows the archive, the naming rule and the link line. Read it first if you have not linked a Mica library from C before.
The library
The library Turnstile counts people walking through a turnstile. Its state
between calls is two unit variables, and every routine that several threads
may call at once touches them inside a synchronized statement:
library Turnstile;
var
passes : int64;
walkers : int64;
exp procedure Walk(people : int64);
var
person : int64;
begin
synchronized walkers := walkers + 1;
for person := 1 to people do
synchronized passes := passes + 1;
end;
exp function Passes() : int64;
begin
synchronized Passes := passes;
end;
exp procedure Reset();
begin
passes := 0;
walkers := 0;
end;
begin
end.A synchronized statement is a critical section the compiler places for
you: in a library built for several threads it always takes its lock, so two
threads that add at the same moment never lose an addition. Reset touches
both counts without one, on purpose, so the header has something to warn
about. The example’s file carries one more reader, Walkers, and a comment
on every line.
The library is built as an archive, for several threads, with its header written beside it:
mica --compile --link --output-modifier archive --emit-header --optimize O2 \
--platform linux,arm64,utf-32 --tasking multicore --project mica.project \
--source Turnstile.mica --build ../build/ThreadsCallMicaThe C side
Each thread’s whole work is one call. The call is the thread’s first, and the library makes the thread a Mica task of its own right there, with its own failure record and its own heap partition, and gives the task back when the thread ends:
#include "Turnstile.h"
#include <pthread.h>
static void *walk_group(void *unused)
{
(void)unused;
Turnstile__Walk(people_per_thread);
return NULL;
}main calls Turnstile__init() once, the library’s body entry, which every
C program calls before its first call into a Mica library. It then starts four
threads with pthread_create, joins them, and prints what the library counted. The program links the library with the standard library
and the multicore runtime floor, mica-dragon-rt-mc-<arch>.a:
cc -std=c23 -pthread -I ../build/ThreadsCallMica -o threadscallmica main.c \
../build/ThreadsCallMica/Turnstile.a /usr/lib/mica-stdlib-arm64.a /usr/lib/mica-dragon-rt-mc-arm64.a -lm
The run
make -C mica-container/examples/ThreadsCallMica runThe program prints the same counts on every run, because no addition is lost:
ThreadsCallMica - 4 C threads call one Mica library
The walks
threads = 4
people per thread = 100000
What the library counted
walks = 4
passes = 400000 of 400000
After the reset
passes = 0Four hundred thousand additions from four threads, and every one of them
counted. The program calls Reset only after it joined its threads, when no
other thread is inside the library.
What the header says
The header --emit-header wrote, Turnstile.h, declares each routine under
its linkage name and states its thread safety in the words of the GNU C
Library’s manual: MT-Safe where the routine touches no state of the library
outside a synchronized statement, and MT-Unsafe with a race: word for
each state it touches without one. Above the routines it states the runtime
the library was built for.
make -C mica-container/examples/ThreadsCallMica header * The library is built for the multicore runtime, on which every C thread that calls it becomes a task of its own on
* its first call, with its own failure record, result region and heap partition and its own place in the lock of every
* 'synchronized' statement, so the library's marks keep the threads apart as a C library's own locks would. Several
* threads may call the library at once, and each routine's comment below states its own thread safety./** MT-Safe */
void Turnstile__Walk(int64_t people);
/** MT-Safe */
int64_t Turnstile__Passes(void);
/** MT-Safe */
int64_t Turnstile__Walkers(void);
/** MT-Unsafe race:passes race:walkers */
void Turnstile__Reset(void);The build says the same where you read it, in a note naming every routine
that is MT-Unsafe and the states it races on. Put Reset’s two counts
inside a synchronized statement and build again, and its comment reads
MT-Safe too. A routine that trains reads race:tape, since a process has
one gradient tape.
The same library, built for one thread
Build Turnstile with --tasking single and its header states the runtime it
was built for and keeps C’s rule for a library that is not thread-safe:
make -C mica-container/examples/ThreadsCallMica header-single * The library is built for the single-carrier runtime, on which every thread that calls it runs as the process's one
* task: the running task, the marks and the memory regions are one set for the whole process, so two threads inside the
* library at once would share them. A program that calls the library from several threads makes them take turns,
* holding one lock of its own around every call.A program that calls such a library from several threads makes them take turns with a mutex of its own, around every call.
The two builds link two floors, and the link keeps them apart. The
multicore archive linked with the floor built for one thread,
mica-dragon-rt-<arch>.a, is refused by the C linker, which names the floor
flavor the archive needs:
make -C mica-container/examples/ThreadsCallMica link-single-floor...: hidden symbol `__mica_floor_tasking_multicore' isn't definedThe mirror is refused the same way: the library built for one thread, linked
with the multicore floor, names __mica_floor_tasking_single, and a Mica
program built with a --tasking other than a library’s is stopped by the
compiler before its linker runs. The C boundary
states every thread rule of a Mica library called from C.
What stays yours
- The stack. A Mica call runs on the stack of the thread that makes it, so a thread you create needs room for the library’s deepest call, as C calls Mica says.
- A thread ends outside a call. Let a thread return from the library
before it calls
pthread_exitor is cancelled: a thread that ends inside a Mica call leaves the program undefined, as the GNU C Library’s manual says of a function it marks AC-Unsafe. MT-Unsafemeans what glibc means. The compiler does not stop a second thread from callingReset. The header tells you not to, the wayman 3 strtokdoes.
Try it
mica-container/devkit/mica-provision gcc # the C side needs a C compiler, installed once
make -C mica-container/examples/ThreadsCallMica runChange thread_count in main.c to sixteen and run it again: the library
counts sixteen walks and 1600000 passes, and the C side still writes nothing
for it.
walks = 16
passes = 1600000 of 1600000Next
Callbacks at the boundary — the other way round: a C library that calls back into your Mica code while your call runs.