Why synchronization is needed
When multiple Threads access the same data, reads and writes can interleave in any order.
Without synchronization the result is a race condition: lost updates, torn reads, or inconsistent state.
Even a single statement like counter++ is not atomic - it is a load, an add and a store.
Two threads running it in parallel can both read the same value, add one, and write back the same result - losing one increment.
Synchronization primitives serialize access to shared data or signal state changes between Threads, so the program behaves as intended.
Solving race conditions
Mutex - protect a critical section
Use a fplMutexHandle to ensure only one Thread executes a code section at a time.
static uint32_t sharedCounter;
void IncrementCounter() {
++sharedCounter;
}
fpl_platform_api bool fplMutexUnlock(fplMutexHandle *mutex)
Unlocks the given mutex.
fpl_platform_api bool fplMutexLock(fplMutexHandle *mutex)
Locks the given mutex and blocks any other threads.
Stores the mutex handle structure.
Signal - notify waiting Threads
Use a fplSignalHandle when one Thread must tell others that something happened (e.g. work is ready).
void ProducerThread() {
}
void ConsumerThread() {
}
fpl_platform_api bool fplSignalSet(fplSignalHandle *signal)
Sets the signal and wakes up the given signal.
fpl_platform_api bool fplSignalWaitForOne(fplSignalHandle *signal, const fplTimeoutValue timeout)
Waits until the given signal is woken up.
#define FPL_TIMEOUT_INFINITE
Infinite timeout constant.
Stores the signal handle structure.
Condition-Variable - wait for a predicate
Use a fplConditionVariable together with a fplMutexHandle when waiters must check a condition under a lock and re-check after wakeup.
static int itemCount;
void Push() {
++itemCount;
}
void Pop() {
while (itemCount == 0) {
}
--itemCount;
}
fpl_platform_api bool fplConditionWait(fplConditionVariable *condition, fplMutexHandle *mutex, const fplTimeoutValue timeout)
Sleeps on the given condition and releases the mutex when done.
fpl_platform_api bool fplConditionSignal(fplConditionVariable *condition)
Wakes up one thread that waits on the given condition.
Stores the condition variable structure.
Semaphore - limit concurrent access
Use a fplSemaphoreHandle to allow up to N Threads into a section at the same time.
void UseResource() {
}
fpl_platform_api bool fplSemaphoreRelease(fplSemaphoreHandle *semaphore)
Increments the semaphore value by one.
fpl_platform_api bool fplSemaphoreWait(fplSemaphoreHandle *semaphore, const fplTimeoutValue timeout)
Waits for the semaphore until it gets signaled or the timeout has been reached.
Stores the semaphore handle structure.
Atomics - lock-free single-value updates
Use atomics when only a single integer or pointer needs to be updated safely, without a Mutex.
static volatile uint32_t counter;
void IncrementCounter() {
}
fpl_platform_api uint32_t fplAtomicAddAndFetchU32(volatile uint32_t *dest, const uint32_t addend)
Adds the addend to destination 32-bit unsigned integer atomically and returns the result after the ad...
Comparison
| Primitive | Purpose | Scope | Cost | Notes |
| fplMutexHandle | Mutual exclusion of a code section | Same process | Low | Only the owner Thread can unlock |
| fplSignalHandle | Notify one or many waiters | Cross-process | Medium | Set/Reset is not Thread-Safe by itself |
| fplConditionVariable | Wait for a predicate under a Mutex | Same process | Medium | Requires a Mutex; supports signal and broadcast |
| fplSemaphoreHandle | Limit N concurrent accessors | Same process | Medium | Any Thread can release; counted permits |
| Atomics | Lock-free op on a single value | Same process | Lowest | One variable at a time; includes a memory barrier |
- Note
- Pick the lightest primitive that fits the problem. Atomics for a single value, a Mutex for a small critical section, a Condition-Variable when waiting on a predicate, a Semaphore for bounded resources, a Signal for cross-process or fan-out notifications.