Implementing a Spinlock Using the Atomic TAS Instruction
Implementing a Spinlock Using the Atomic TAS Instruction
Building a Spinlock from Scratch with Atomic TAS
Implementing a Minimal Mutex Using Test-and-Set
Inside Spinlocks: TAS, Atomicity, and Busy Waiting
Atomic Operations and Spinlocks: Thread Synchronization in C
From Atomic Instructions to Locks: A Complete Guide to TAS and Spinlocks
Hands-On Spinlock Implementation: tryLock, lockAcquire, and lockRelease
Your First Spinlock: A TAS-Based Implementation in C
Atomic Exchange and Thread Mutual Exclusion: A Guide to Implementing Spinlocks
Suppose we are given a TAS (Take-And-Set) function. This operation returns the old value stored in memory and atomically replaces it with a new value. Being atomic means that no other thread can observe the intermediate state; the entire read–write pair happens as one indivisible step.
In C++, the standard library function std::exchange behaves the same way logically, but is not atomic. Hardware-level atomicity is required for synchronization primitives.
int TAS(int* memory, int newVal) {
int old = *memory;
*memory = newVal;
return old;
}
We want to use this primitive to implement a simple spinlock with:
lockAcquire()lockRelease()
Threads will call these functions to protect access to a shared variable:
typedef struct {
int lock;
} lockType;
typedef struct {
int val;
} threadArgType;
void threadFunc(void* arg) {
lockAcquire((static_cast<lockType*>arg)->lock);
(static_cast<threadArgType*>arg)->val++;
lockRelease((static_cast<lockType*>arg)->lock);
}
Implementing tryLock
A tryLock function attempts to acquire the lock once. If the lock was free (value = 0), TAS sets it to 1 and returns the old value (0). If the lock was already taken, TAS returns 1. The tryLock function is not blocking – it returns immediately.
So tryLock() should succeed only when TAS returns 0:
enum {
UNLOCKED = 0,
LOCKED = 1
}
int tryLock(lockType* lock) {
// Returns 1 if previously locked, 0 if previously unlocked
int old = TAS(lock->lock, LOCKED);
return (old == UNLOCKED); // true (1) = acquired successfully
}
Implementing lockAcquire()
A normal lock acquisition should “spin” until tryLock() succeeds. This is called a spinlock because the CPU will busy-wait. We can introduce a brief sleep when necessary. For instance, sleep(0) doesn’t actually pause execution but yields the CPU, allowing other threads to run.
It is commonly used to implement spin locks for mutual exclusion across threads.
void lockAcquire(lockType* lock) {
while (!tryLock(lockType* lock)) {
// spin until lock becomes available
}
}
another implementation:
void lockAcquire(lockType* lock) {
do {
if (tryLock(lockType* lock)) {
break;
}
} while (1);
}
Unrolling tryLock:
void lockAcquire(lockType* lock) {
do {
int old = TAS(lock->lock, LOCKED);
// the lock has been set to LOCKED no matter if a lock has been aquired
if (old == UNLOCKED) {
break;
}
} while (1);
}
This is the simplest possible implementation using TAS. In real systems we may add pause instructions or backoff strategies, but the basic idea remains the same.
Implementing lockRelease()
To release the lock, the holder simply writes 0 to the lock variable. Because TAS is “set to new value and return old value”, it works fine for releasing too:
void lockRelease(lockType* lock) {
TAS(lock->lock, UNLOCKED);
}
Alternatively, a simple atomic store is enough, but since TAS is our only tool, we reuse it. Keep in mind that releasing the lock twice is harmless here, since setting it to UNLOCKED=0 again has no side effects.
Summary
Using only an atomic TAS instruction, we implemented:
- A
tryLock()attempt - A
lockAcquire()spinlock - A
lockRelease()unlock operation
This style of lock is foundational for understanding low-level concurrency, memory ordering, and how higher-level mutex libraries are built.
C/C++ Programming
- Understanding std::transform_reduce in Modern C++
- Implement a Lock Acquire and Release in C++
- Detecting Compile-time vs Runtime in C++: if consteval vs std::is_constant_evaluated()
- C++ Forward References: The Key to Perfect Forwarding
- Understanding dynamic_cast in C++: Safe Downcasting Explained
- C vs C++: Understanding the restrict Keyword and its Role in Optimization
- C++ Lvalue, Rvalue and Rvalue References
- C++ assert vs static_assert
- Why auto_ptr is Deprecated in C++?
- C++ What is the consteval? How is it different to const and constexpr?
- Tutorial on C++ std::move (Transfer Ownership)
- const vs constexpr in C++
- Tutorial on C++ Ranges
- Tutorial on C++ Smart Pointers
- Tutorial on C++ Future, Async and Promise
- The Memory Manager in C/C++: Heap vs Stack
- The String Memory Comparision Function memcmp() in C/C++
- Modern C++ Language Features
- Comparisions of push_back() and emplace_back() in C++ std::vector
- C++ Coding Reference: is_sorted_until() and is_sorted()
- C++ Coding Reference: iota() Setting Incrementing Values to Arrays or Vectors
- C++ Coding Reference: next_permutation() and prev_permutation()
- C++ Coding Reference: count() and count_if()
- C++ Code Reference: std::accumulate() and Examples
- C++ Coding Reference: sort() and stable_sort()
- The Next Permutation Algorithm in C++ std::next_permutation()
–EOF (The Ultimate Computing & Technology Blog) —
758 wordsLast Post: Cambridge Science Park: Microsoft, AMD and Raspberry Pi (Neighbours)
Next Post: Introduction to Parquet Files, Read/Write using Python