Simple spinlocks implemented with only C atomics and no syscalls. The point is to decouple from the C runtime library
as much as possible while also guaranteeing a lack of syscalls or other function imports. This is useful when making
an OS-independent library (-ffreestanding) or when undefined behavior is undesirable.
It is important to remember that any thread waiting on a spinlock is wasting cpu cycles while it waits; it's cpu
time is not yielded to the OS, and other work cannot be done on this core by other threads in the meantime. Normally,
when used responsibly, this is the most efficient option as merely switching rings has a tremendous cost, much less
the thread sleeping and getting rescheduled. Any code guarded by the lock should be as short and fast as possible.
You should try to avoid memory allocations when possible, though that's not always possible or convenient, depending
on the situation.
I don't claim to actually understand memory barriers on x86-64. The values used internally were chosen by empirical
testing of the different options with a hundred threads aggressively fighting over one variable.
This is an optional file within sti and must be included separately:
#include "sti/spinlock.h"
There is a convenience typedef for the lock variable itself:
typedef _Atomic u32 spinlock_t;
static inline void spin_lock(spinlock_t* sl)
Blocks until it can atomically set sl from 0 to 1 with the proper memory ordering. Contains no syscalls,
and does not yield thread time to the OS.
static inline void spin_unlock(spinlock_t* sl)
Atomically sets sl to 0 with the proper memory ordering.
Usage
Using a hash table in a thread-safe manner:
typedef struct {
spinlock_t ages_lock;
HT(int) ages;
} People;
void happy_birthday(People* o, char* name) {
spin_lock(&o->entries_lock);
int age;
if(!HT_get(&o->ages, name, &age)) {
HT_set(&o->ages, name, age + 1);
}
else {
panic("No such person: %s", name);
}
spin_unlock(&o->entries_lock);
}