How Abseil’s Mutex Implements Cooperative vs Non‑Cooperative Scheduling Modes

Abseil’s absl::Mutex distinguishes between cooperative and non‑cooperative scheduling modes to determine whether waiting threads yield to user‑level schedulers or block directly on kernel synchronization primitives.

The absl::Mutex synchronization primitive in the abseil/abseil-cpp repository provides a hybrid threading model that unifies traditional OS thread blocking with user‑level cooperative multitasking. Understanding how Abseil Mutex cooperative vs non‑cooperative scheduling modes work is essential for low‑level library development and high‑performance computing where fibers or coroutines interact with standard mutexes.

Scheduling Mode Fundamentals

At the core of this implementation lies the SchedulingMode enum defined in absl/base/internal/scheduling_mode.h (lines 30–41). This enumeration provides two distinct values that control thread rescheduling behavior during mutex contention:

  • SCHEDULE_KERNEL_ONLY (value 0): Forces the thread to block exclusively on host‑OS primitives. The waiting thread enters a kernel‑space futex or equivalent wait state without yielding to user‑level schedulers.
  • SCHEDULE_COOPERATIVE_AND_KERNEL: Allows the waiting thread to be rescheduled by Abseil’s cooperative scheduling facilities. While waiting, the thread may yield execution to other cooperative fibers or coroutines before the kernel wakes it.

The mode is stored per‑thread within the base_internal::ThreadIdentity structure, enabling each thread to carry its scheduling preference independently of the mutex object itself.

Core Implementation Components

The mutex delegates its waiting logic to three low‑level subsystems that abstract the cooperative vs. non‑cooperative distinction.

PerThreadSem Abstraction

The PerThreadSem class in absl/synchronization/internal/per_thread_sem.h provides a counting‑semaphore‑like primitive for individual threads. Its static Wait(KernelTimeout t) method drives the actual blocking operation:

class PerThreadSem {
  // ...
  static inline bool Wait(KernelTimeout t);  // Blocks until count > 0
};

Under the hood, Wait() invokes the C symbol AbslInternalPerThreadSemWait. On POSIX systems, this translates to a futex wait for non‑cooperative modes, while cooperative platforms may instead call absl::base::scheduling::Yield to surrender control to the cooperative scheduler.

Low‑Level Scheduling Helpers

The absl/base/internal/low_level_scheduling.h header exposes functions that query and manipulate the current thread’s scheduling capability. The SchedulingModeIsCooperative() function returns true only when the calling thread permits cooperative rescheduling. Additionally, the SchedulingGuard RAII wrapper allows temporary mode changes by modifying the thread’s ThreadIdentity scheduling state.

Mutex State Encoding

Internally, the mutex state (mu_) encodes both the lock ownership status and the scheduling mode for queued waiters. According to comments in spinlock.h (line 289), bit 1 of the internal state specifically flags whether the lock uses cooperative scheduling. This ensures that all waiters in the queue adhere to the same scheduling discipline as the first blocking thread.

The Lock Acquisition Path

When a thread calls Mutex::Lock() or Mutex::LockShared(), the implementation follows a two‑phase approach.

Fast Path

The fast path attempts atomic acquisition using compare‑and‑swap operations. If the mutex is uncontended, the thread acquires the lock immediately without involving scheduling modes or kernel primitives.

Slow Path and LockSlowLoop

If contention exists, control transitions to LockSlowLoop in absl/synchronization/mutex.cc. Within this slow path, the implementation performs the following steps:

  1. Mode Inspection: The current thread’s scheduling mode is retrieved from base_internal::ThreadIdentity.
  2. Cooperative Wait: If the mode is SCHEDULE_COOPERATIVE_AND_KERNEL, the thread invokes PerThreadSem::Wait() with a timeout that permits cooperative yielding. This allows other fibers to execute while the mutex remains held.
  3. Non‑Cooperative Wait: If the mode is SCHEDULE_KERNEL_ONLY, the same PerThreadSem::Wait() call executes, but the underlying AbslInternalPerThreadSemWait uses a pure kernel wait (e.g., futex) that suspends the thread without yielding to the cooperative scheduler.

This hybrid design ensures that the kernel always provides the ultimate contention primitive, while the cooperative layer merely determines whether the thread may be descheduled during the wait.

Practical Usage Examples

The scheduling mode is not passed as a parameter to Mutex methods; instead, it is derived from the thread’s current execution context.

Cooperative Mode with Fibers

When running on a cooperative scheduler (such as Abseil’s fibers), threads automatically inherit the cooperative scheduling mode:

#include "absl/synchronization/mutex.h"

void CooperativeWorker() {
  // Running on a fiber; thread identity automatically marks this as cooperative
  absl::Mutex mu;
  
  mu.Lock();  // May yield to other fibers while waiting
  // Critical section
  mu.Unlock();
}

In this scenario, mu.Lock() may invoke absl::base::scheduling::Yield via PerThreadSem::Wait(), allowing other cooperative threads to make progress before the kernel wakes this waiter.

Non‑Cooperative Mode with SchedulingGuard

For plain OS threads or low‑level scheduler implementations, explicitly disable cooperative rescheduling using SchedulingGuard:

#include "absl/synchronization/mutex.h"
#include "absl/base/internal/scheduling_mode.h"

void NonCooperativeWorker() {
  // Force kernel‑only scheduling for this scope
  absl::base_internal::SchedulingGuard guard(
      absl::base_internal::SchedulingMode::SCHEDULE_KERNEL_ONLY);
  
  absl::Mutex mu;
  mu.Lock();  // Blocks on futex; no cooperative yielding
  // Critical section
  mu.Unlock();
}

This pattern ensures strict kernel‑only semantics, preventing the cooperative scheduler from interfering with the thread’s wait state.

Safety Constraints and Design Guarantees

The implementation enforces strict safety invariants regarding mode nesting. As documented in absl/base/internal/scheduling_mode.h (lines 46–48), non‑cooperative resources must never be nested beneath cooperative ones. Attempting to acquire a cooperative mutex while holding a non‑cooperative lock constitutes a programming error that triggers an abort in debug builds. This constraint prevents deadlocks where a cooperative scheduler might attempt to reschedule a thread that holds a kernel‑only resource.

Summary

  • Abseil’s absl::Mutex supports two scheduling modes: SCHEDULE_COOPERATIVE_AND_KERNEL and SCHEDULE_KERNEL_ONLY.
  • PerThreadSem abstracts the wait mechanism, choosing between cooperative yielding and kernel futex waits based on the thread’s current mode.
  • LockSlowLoop in mutex.cc inspects the thread’s ThreadIdentity to select the appropriate blocking strategy during contention.
  • SchedulingGuard provides RAII semantics for temporarily forcing non‑cooperative behavior on cooperative threads.
  • Safety invariants prohibit nesting cooperative locks under non‑cooperative ones to prevent scheduler deadlocks.

Frequently Asked Questions

What is the default scheduling mode for absl::Mutex?

By default, absl::Mutex derives the scheduling mode from the calling thread’s ThreadIdentity. Threads running on standard OS threads without an active cooperative scheduler typically default to SCHEDULE_KERNEL_ONLY, while threads managed by Abseil’s fiber or cooperative scheduling infrastructure automatically use SCHEDULE_COOPERATIVE_AND_KERNEL.

Can I mix cooperative and non-cooperative mutexes in the same program?

Yes, provided you respect the nesting constraints. You may acquire a non‑cooperative mutex while holding a cooperative one, but the reverse is forbidden. Non‑cooperative resources must never be held when attempting to acquire cooperative resources to avoid violating scheduler assumptions.

How does Abseil implement the actual wait operation on Linux?

On Linux, the PerThreadSem::Wait() function calls AbslInternalPerThreadSemWait, which uses the futex system call (FUTEX_WAIT) for non‑cooperative waits. When cooperative mode is active, the implementation may instead call absl::base::scheduling::Yield to surrender control to the user‑level scheduler before falling back to the futex if necessary.

What is the performance impact of cooperative scheduling mode?

Cooperative mode adds minimal overhead when the mutex is uncontended (fast path). Under contention, cooperative mode may reduce latency for fiber‑based workloads by allowing other cooperative threads to execute during the wait, though it introduces a slight indirection through the PerThreadSem abstraction compared to raw futex operations.

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 →