# How Abseil's SpinLock Provides Async Signal Safety: Kernel-Only Scheduling Mode Explained

> Discover how Abseil's SpinLock ensures async signal safety in kernel-only scheduling mode by using atomic operations and avoiding cooperative scheduling. Learn the key requirements for safe usage.

- Repository: [Abseil/abseil-cpp](https://github.com/abseil/abseil-cpp)
- Tags: internals
- Published: 2026-07-12

---

**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`](https://github.com/abseil/abseil-cpp/blob/main/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:

```cpp
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`](https://github.com/abseil/abseil-cpp/blob/main/absl/base/internal/spinlock.h) explicitly states that async signal safety depends on blocking signals before acquiring the lock:

```cpp
// 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`](https://github.com/abseil/abseil-cpp/blob/main/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

```cpp
#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

```cpp
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:

```cpp
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`](https://github.com/abseil/abseil-cpp/blob/main/spinlock.h)**: Contains the core `SpinLock` class, `SchedulingMode` handling, fast-path lock/unlock implementations, and async-signal-safety documentation
- **[`scheduling_mode.h`](https://github.com/abseil/abseil-cpp/blob/main/scheduling_mode.h)**: Defines the `SchedulingMode` enum (`SCHEDULE_KERNEL_ONLY` vs. cooperative scheduling)
- **[`spinlock_wait.h`](https://github.com/abseil/abseil-cpp/blob/main/spinlock_wait.h)**: Provides helpers for the contended (slow) path; explicitly avoided in signal-safe scenarios
- **[`low_level_scheduling.h`](https://github.com/abseil/abseil-cpp/blob/main/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`](https://github.com/abseil/abseil-cpp/blob/main/absl/base/internal/spinlock.h) with scheduling definitions in [`scheduling_mode.h`](https://github.com/abseil/abseil-cpp/blob/main/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.