Modules

Thread

moduleSource

Available on macOS, Linux, Windows, Android

#import "Thread";

Threads and what they need to cooperate: Thread to run a procedure on its own thread, Mutex, Condition_Variable and Semaphore to coordinate, and Thread_Group for a pool of workers.

Not available on wasm: it has no operating-system threads.

Example↗
Shared :: struct { mutex: Mutex; total: int; }

add_many :: (thread: *Thread) -> s64 {
    shared := cast(*Shared) thread.data;
    for 1..1000 {
        lock(*shared.mutex);
        shared.total += 1;
        unlock(*shared.mutex);
    }
    return 0;
}

shared: Shared;
init(*shared.mutex);
threads: [4] Thread;
for *thread: threads {
    assert(thread_init(thread, add_many));
    thread.data = *shared;
    thread_start(thread);
}
for *thread: threads {
    assert(thread_is_done(thread, -1));
    thread_deinit(thread);
}
assert(shared.total == 4000);
destroy(*shared.mutex);

Module parameters#

LOAD_THREAD_GROUP := true
Includes Thread_Group.
CACHE_LINE_SIZE: s32 = 64
The cache line size in bytes, which pads the records of workers.
DO_RACE_DETECTOR := false
Reserved for a race detector; it must stay false.
SET_THREAD_NAMES := true
Gives each thread's name to the operating system.

Procedures#

General#

(thread: *Thread, proc: Thread_Proc, temporary_storage_size: s32 = 16384, starting_storage: *Temporary_Storage = null, name := "") -> bool

Prepares thread to run proc. Start it with thread_start. Returns false when proc is null, the storage size is negative or an allocation fails.

hello :: (thread: *Thread) -> s64 { return 0; }
thread: Thread;
assert(thread_init(*thread, hello, name = "hello"));
thread_start(*thread);
assert(thread_is_done(*thread, -1));
thread_deinit(*thread);
↗

Native Windows#

init7 overloadsSource
(m: *Mutex, desired_name := "", desired_order: Mutex_Order = -1)
(cv: *Condition_Variable, desired_name := "")
(s: *Semaphore, initial_value: s32 = 0)
(m: *Mutex, desired_name := "", desired_order: Mutex_Order = -1)
(cv: *Condition_Variable, desired_name := "")
(s: *Semaphore, initial_value: s32 = 0)
(group: *Thread_Group, num_threads: s32, group_proc: Thread_Group_Proc, enable_work_stealing := false, temporary_storage_size: s32 = 16384, group_name := "")

Sets up a mutex. It is recursive: the thread that holds it can lock it again, and has to unlock it as many times.

lock2 overloadsSource
(m: *Mutex)
(m: *Mutex)

Takes the mutex, waiting until no other thread holds it. Pair every lock with an unlock.

unlock2 overloadsSource
(m: *Mutex)
(m: *Mutex)

Gives the mutex back. Call it from the thread that locked it.

Lifecycle#

(thread: *Thread, milliseconds: s32 = 0) -> bool

Whether thread has finished.

count := 0;
worker :: (thread: *Thread) -> s64 {
    (cast(*int) thread.data).* = 7;
    return 0;
}

thread: Thread;
assert(thread_init(*thread, worker));
thread.data = *count;
thread_start(*thread);
assert(thread_is_done(*thread, -1)); // wait for it
thread_deinit(*thread);
assert(count == 7);
↗
(thread: *Thread)

Cleans up after a thread that has finished. Wait for it first with thread_is_done(*thread, -1); after this the Thread can be reused or dropped.

Group#

startsince 0.31.0Source
(group: *Thread_Group)

Starts the workers of group. Work added before this waits in the queues. Calling it again does nothing.

add_worksince 0.31.0Source
(group: *Thread_Group, work: *void, logging_name := "")

Queues work for one of the group's workers; group_proc receives the same pointer. The work must stay valid until it comes back from get_completed_work. Send work to a group that has been set up and not shut down.

shutdownsince 0.31.0Source
(group: *Thread_Group, timeout_milliseconds: s32 = -1) -> bool

Stops the workers after they finish what is queued and frees the group. Returns false if they did not all stop within timeout_milliseconds (-1 waits as long as it takes).

Idle#

(group: *Thread_Group, timeout_milliseconds: s32 = -1) -> bool

Waits until every piece of work added to group has been run by a worker, or until timeout_milliseconds pass (-1 waits as long as it takes). Returns false on a timeout. The group has to be started, and has to have a live worker to run the work: if every worker stopped with work still queued, only a timeout ends the wait.

Counter :: struct { mutex: Mutex; total: s64; }

count :: (group: *Thread_Group, thread: *Thread, work: *void) -> Thread_Continue_Status {
    counter := cast(*Counter) group.data;
    lock(*counter.mutex);
    counter.total += 1;
    unlock(*counter.mutex);
    return .CONTINUE;
}

counter: Counter;
init(*counter.mutex);
group: Thread_Group;
group.logging = false;
init(*group, 4, count);
group.data = *counter;
start(*group);
for 1..100 add_work(*group, null);

assert(wait_for_all(*group));
assert(counter.total == 100);
assert(shutdown(*group));
destroy(*counter.mutex);
↗

Types#

:: enum

What wait_for returns: .SUCCESS when it took a count, .TIMEOUT when the time ran out and .ERROR when the wait failed.

SUCCESS
TIMEOUT
ERROR
:: struct

A lock that lets one thread at a time into a piece of code. Set it up with init, take it with lock, give it back with unlock. It can be locked again by the thread that already holds it.

:: struct

A counter threads can wait on. signal adds one; wait_for takes one, waiting if there are none. Handy for handing work between threads.

event: W.HANDLE
:: struct

An operating-system thread. Set it up with thread_init, fill in data if the procedure needs it, run it with thread_start, wait with thread_is_done and clean up with thread_deinit. The Thread must stay where it is until thread_deinit. The procedure gets a pointer to it, with data, the thread's index and its own temporary storage.

data: *void
workspace: Workspace
starting_context: #Context
starting_temporary_storage: Temporary_Storage
allocator_used_for_temporary_storage: Allocator
:: enum

What a Thread_Group procedure returns after one piece of work: .CONTINUE for more, .STOP to end that worker.

STOP
CONTINUE
:: struct

A pool of worker threads that run one procedure over pieces of work. Set it up with init, queue work with add_work, start the workers and collect results with get_completed_work. shutdown stops the workers.

data: *void
name: string
logging: := true
time_proc: () -> float64
worker_start: (group: *Thread_Group, thread: *Thread, worker_index: s32)
Set before start to run something on each worker thread, once, before its first piece of work. worker_index is the worker's place in the group, 0 up to the number of threads minus one. A good place to build state the worker keeps.
allocator: Allocator
worker_info: [] Worker_Info
worker_info_data_to_free: *void
next_worker_index: s32
outstanding: s64
Work added and not yet finished, and what wait_for_all sleeps on.
idle_mutex: Mutex
idle_changed: Condition_Variable
initted: := false
started: := false
should_exit: := false
Work :: struct { input: int; result: int; }

square :: (group: *Thread_Group, thread: *Thread, work: *void) -> Thread_Continue_Status {
    item := cast(*Work) work;
    item.result = item.input * item.input;
    return .CONTINUE;
}

group: Thread_Group;
group.logging = false;
init(*group, 2, square);
items: [4] Work;
for *item, i: items {
    item.input = i + 1;
    add_work(*group, item);
}
start(*group);

assert(wait_for_all(*group));
assert(get_completed_work(*group).count == items.count);
assert(shutdown(*group));
assert(items[3].result == 16);
↗

Constants#