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 core SpinLock class, SchedulingMode handling, fast-path lock/unlock implementations, and async-signal-safety documentation
  • scheduling_mode.h: Defines the SchedulingMode enum (SCHEDULE_KERNEL_ONLY vs. cooperative scheduling)
  • spinlock_wait.h: Provides helpers for the contended (slow) path; explicitly avoided in signal-safe scenarios
  • low_level_scheduling.h: Implements SchedulingGuard primitives that are bypassed when SCHEDULE_KERNEL_ONLY is selected

Summary

  • Use SCHEDULE_KERNEL_ONLY: Construct SpinLock with 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 SlowLock or SlowUnlock, which invoke futexes and other non-reentrant mechanisms
  • File locations: Core logic resides in absl/base/internal/spinlock.h with scheduling definitions in scheduling_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →