How Abseil's SpinLock Provides Async Signal Safety: Kernel-Only Scheduling Mode Explained
Abseil's SpinLock achieves async signal safety when constructed with SCHEDULE_KERNEL_ONLY by restricting operations to atomic compare-exchange sequences and avoiding cooperative scheduling machinery, provided signals are blocked while the lock is held.
The absl::base_internal::SpinLock class in the abseil/abseil-cpp repository provides a lightweight synchronization primitive designed for high-performance scenarios, including use within asynchronous signal handlers. When configured properly, this lock avoids all non-reentrant operations—such as memory allocation, futex syscalls, and fiber scheduling—that would otherwise make it unsafe for signal contexts. Understanding the specific architectural choices in absl/base/internal/spinlock.h reveals how the implementation guarantees safety through minimal atomic operations and explicit scheduling constraints.
Architectural Foundations of Async Signal Safety
The async signal safety of SpinLock rests on three deliberate design choices that eliminate external dependencies and reentrant code paths.
Kernel-Only Scheduling Mode
The constructor SpinLock(SchedulingMode mode) encodes the scheduling policy directly into the lock word. When instantiated with SCHEDULE_KERNEL_ONLY, the lock operates in non-cooperative mode, clearing the kSpinLockCooperative bit from the internal state:
constexpr explicit SpinLock(SchedulingMode mode)
: lockword_(IsCooperative(mode) ? kSpinLockCooperative : 0) {
RegisterWithTsan();
}
Because this bit is absent, the lock never engages Abseil's cooperative scheduling machinery—specifically avoiding SchedulingGuard and fiber-related operations. Only the OS kernel may preempt the thread, which is permissible inside a signal handler according to POSIX standards.
Lock-Word Atomic Operations
The lock word is implemented as a single std::atomic<uint32_t> that stores only simple flag bits (kSpinLockHeld, kSpinLockCooperative, kSpinLockDisabledScheduling) and an optional wait-time field. All state transitions occur through atomic compare-exchange (compare_exchange_strong) and plain loads/stores:
- No memory allocation occurs during lock acquisition or release
- No mutexes or recursive locks invoke non-async-signal-safe library functions
- Atomic operations are guaranteed async signal safe on all supported platforms
This design ensures that the fast path (TryLockImpl) consists solely of a single load followed by a compare_exchange operation.
Avoiding the Slow Path
The implementation distinguishes between fast and slow paths through TryLockInternal and SlowLock/SlowUnlock. The header documentation in absl/base/internal/spinlock.h explicitly states that async signal safety depends on blocking signals before acquiring the lock:
// SpinLock with a SchedulingMode::SCHEDULE_KERNEL_ONLY is async
// signal safe. If a spinlock is used within a signal handler, all code that
// acquires the lock must ensure that the signal cannot arrive while they are
// holding the lock. Typically, this is done by blocking the signal.
By blocking the signal while the lock is held, you guarantee that the signal handler never executes the slow path that could invoke futexes, syscalls, or other non-reentrant code. The handler can only execute the atomic fast path, which is safe for reentrant execution.
Implementation Details in the Abseil Source
The safety mechanism is enforced through bitwise operations in the lock word. When IsCooperative(mode) returns false (as with SCHEDULE_KERNEL_ONLY), the cooperative bit remains cleared, and subsequent operations in TryLockInternal skip scheduling disable/resume logic.
According to the source code at lines 79-103 of spinlock.h, the fast path checks the cooperative flag before deciding whether to interact with SchedulingGuard. With the kernel-only mode, these checks short-circuit, keeping the critical section limited to pure atomic operations on the lockword_ member.
Practical Usage Patterns
To safely use SpinLock in signal handlers, you must block the relevant signals before acquiring the lock in non-handler code. This prevents the handler from attempting to acquire an already-held lock and triggering the slow path.
Blocking Signals Before Locking
#include <signal.h>
#include "absl/base/internal/spinlock.h"
static absl::base_internal::SpinLock kLock(
absl::base_internal::SCHEDULE_KERNEL_ONLY);
void SafeHandler(int signo) {
// Safe to acquire because the signal was blocked by the main thread
absl::base_internal::SpinLockHolder h(kLock);
// Perform only async-signal-safe actions (e.g., setting flags)
}
int main() {
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGUSR1);
// Block SIGUSR1 before entering the protected region
pthread_sigmask(SIG_BLOCK, &block, &old);
{
absl::base_internal::SpinLockHolder h(kLock);
// Critical work that must not be interrupted by the handler
}
// Restore original mask so the handler may run safely
pthread_sigmask(SIG_SETMASK, &old, nullptr);
return 0;
}
Using RAII for Automatic Management
void ProcessData() {
static absl::base_internal::SpinLock lock(
absl::base_internal::SCHEDULE_KERNEL_ONLY);
absl::base_internal::SpinLockHolder guard(lock); // Acquires on construction
// ... critical section ...
} // guard destructor unlocks atomically
Cooperative Mode (Not Signal Safe)
The default constructor creates a cooperative lock that is not async signal safe:
absl::base_internal::SpinLock lock; // Defaults to cooperative mode
{
absl::base_internal::SpinLockHolder h(lock);
// Safe for general use, but undefined behavior in signal handlers
}
Key Source Files
The implementation spans several internal headers in absl/base/internal/:
spinlock.h: Contains the coreSpinLockclass,SchedulingModehandling, fast-path lock/unlock implementations, and async-signal-safety documentationscheduling_mode.h: Defines theSchedulingModeenum (SCHEDULE_KERNEL_ONLYvs. cooperative scheduling)spinlock_wait.h: Provides helpers for the contended (slow) path; explicitly avoided in signal-safe scenarioslow_level_scheduling.h: ImplementsSchedulingGuardprimitives that are bypassed whenSCHEDULE_KERNEL_ONLYis selected
Summary
- Use
SCHEDULE_KERNEL_ONLY: ConstructSpinLockwith this specific mode to disable cooperative scheduling and fiber interactions - Block signals while holding: Prevent the signal handler from executing the slow path by masking signals before acquiring the lock in normal code
- Atomic-only operations: The implementation relies on
std::atomic<uint32_t>compare-exchange operations with no memory allocation or syscalls in the fast path - Avoid the slow path: Signal handlers must never trigger
SlowLockorSlowUnlock, which invoke futexes and other non-reentrant mechanisms - File locations: Core logic resides in
absl/base/internal/spinlock.hwith scheduling definitions inscheduling_mode.h
Frequently Asked Questions
What makes SpinLock async signal safe while std::mutex is not?
std::mutex typically allocates memory and may invoke futexes or other blocking kernel operations that are not async signal safe. In contrast, SpinLock with SCHEDULE_KERNEL_ONLY restricts itself to atomic compare-exchange operations on a primitive integer, avoiding all library calls that could deadlock or corrupt state when interrupted by a signal.
Why must signals be blocked while holding the lock?
Blocking signals prevents the handler from executing while the lock is held, ensuring the handler never encounters a contended lock. If the handler ran while the main thread held the lock, it might need to execute SlowLock, which invokes futex syscalls and cooperative scheduling primitives—operations undefined in signal contexts. By blocking the signal, the handler only runs when the lock is free, guaranteeing it always executes the fast atomic path.
What happens if I use the default cooperative mode in a signal handler?
Using the default cooperative mode in a signal handler results in undefined behavior. The default constructor sets the kSpinLockCooperative bit, causing TryLockInternal to interact with SchedulingGuard and potentially fiber schedulers. These operations may call non-reentrant library functions or manipulate shared state unsafely, leading to deadlocks or memory corruption.
Is SpinLock wait-free or lock-free?
SpinLock provides lock-free progress for the fast path but not wait-free guarantees. The TryLockImpl operation uses atomic compare-exchange, which is lock-free. However, the slow path may spin or yield, meaning threads can block indefinitely under contention. For async signal safety, you must ensure the slow path is never taken by the signal handler through proper signal masking.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →