package core.sync
Barrier :: struct {
mutex: Mutex
cond: Condition_Variable
index: i32
generation: i32
thread_count: i32
}
Represents a generational barrier, so the same barrier can be used safely multiple times.
Channel :: struct (T: type_expr) {
_buf: [..] T
_mutex: Mutex
_condvar: Condition_Variable
_is_open: bool
}
Condition_Variable :: struct {
mutex: Mutex
queue: &Condition_Variable.Node
}
A condition variable is used to implement a queue of threads waiting for a condition to be true. Each thread joins the queue using condition_wait
. Then, another thread can signal that the condition has changed and can "wake up" the first thread in the queue using condition_signal
. Alternatively, all threads can be woken up using condition_broadcast
.
Condition variables are generally used to prevent spin checking a condition and waiting for it to change. Instead, the thread joins a wait-queue, and leave it up to another thread to wake it up to continue processing. However sadly, in WebAssembly this is not possible because with the atomic_wait and atomic_notify instructions, which currently are not supported by any runtime outside of the browser.
Mutex :: struct {
lock: i32
owner: i32
}
A mutex represents a resource that can only be held by one thread at a time. It is used to create sections of code that only one thread can be in at a time.
Mutexes in WebAssembly are very cheap, because they simply use the atomic_cmpxchg intrinsic to operate. This only uses memory, so no real resource allocation is necessary.
lock
has two states: 0, and 1. 0 means unlocked, 1 means locked
To lock it:
Try to store 1 if the value was already 0.
Otherwise, if it was already 1, wait until it goes to 0.
To unlock it:
Atomically set it to 0.
Notify at most 1 other thread about this change.
MutexGuard :: struct (T: type_expr) {
_: ARRAY TYPE
}
Represents a "guarded" value, i.e. one that is protected by a mutex.
The only way to access the value inside is by using the with
method and passing in a code block that accepts a pointer to the value. This way, there is no way to access the value without locking a mutex. (Unless of course, you store the pointer somewhere else, but then you are just being a bad citizen of the programming language ;) ).
Once :: struct {
done: bool
mutex: Mutex
}
Represents something will only happen once.
Semaphore :: struct {
mutex: Mutex
counter: i32
}
A semaphore represents a counter that can only be incremented and decremented by one thread at a time. "Waiting" on a semaphore means decrementing the counter by 1 if it is greater than 0, otherwise waiting until the counter is incremented. "Posting" on a semaphore means incrementing the counter by a certain value, in turn releasing other threads that might have been waiting for the value to change.
Semaphores are generally used for controlling access to shared resources. For a contrived example, say only 4 threads can use a given network connection at a time. A semaphore would be created with a value of 4. When a thread wants to use the network connection, it would use semaphore_wait
to obtain the resource, or wait if the network is currently available. When it is done using the network, it would call semaphore_post
to release the resource, allowing another thread to use it.
barrier_init :: (b: &Barrier, thread_count: i32) -> void
Initializes a new generational barrier with thread_count
threads.
barrier_wait :: (b: &Barrier) -> void
Signals that a thread has reached the barrier. The last thread to reach the barrier will wake up all other threads.
condition_broadcast :: (c: &Condition_Variable) -> void
Wakes up all threads from the wait-queue.
condition_destroy :: (c: &Condition_Variable) -> void
Destroys a condition variable.
condition_init :: (c: &Condition_Variable) -> void
Initializes a new condition variable.
condition_signal :: (c: &Condition_Variable) -> void
Wakes up one thread from the wait-queue.
condition_wait :: (c: &Condition_Variable, m: &Mutex) -> void
Enters the thread in the wait-queue of the condition variable. If m
is not null, the mutex will first be released before entering the queue, and then relocked before returning.
critical_section :: macro (m: &Mutex, body: Code) -> i32
Abstracts the pattern decribed in scoped_mutex by automatically calling scoped_mutex in the block of code given.
m: sync.Mutx;
sync.mutex_init(&m);
sync.critical_section(&m) {
// Everything here is done by one thread at a time.
}
mutex_lock :: (m: &Mutex) -> void
Locks a mutex. If the mutex is currently held by another thread, this function enters a spin loop until the mutex is unlocked. In a JavaScript based implementation, the __atomic_wait intrinsic is used to avoid having to spin loop.
mutex_unlock :: (m: &Mutex) -> void
Unlocks a mutex, if the calling thread currently holds the mutex. In a JavaScript based implementation, the __atomic_notify intrinsic is used to wake up one waiting thread.
scoped_mutex :: macro (m: &Mutex) -> void
Helpful macro for making a particular block be protected by a macro.
m: sync.Mutx;
sync.mutex_init(&m);
{
sync.scoped_mutex(&m);
// Everything here is done by one thread at a time.
}
semaphore_init :: (s: &Semaphore, value: i32) -> void
Initializes a semaphore with the specified value.
semaphore_post :: (s: &Semaphore, count: i32) -> void
Increment the counter in a semaphore by count
.