Bilibili C++ Interview: Differences Between Mutex and Spin Lock, and Their Use Cases

Bilibili C++ Interview: Differences Between Mutex and Spin Lock, and Their Use CasesIn multithreaded programming, synchronization mechanisms are essential for ensuring safe access to shared resources.Mutexes and Spin Locks, as the two most classic types of locks, are widely used in various systems and frameworks.This article will comprehensively analyze the differences between the two from the perspectives of underlying implementation principles, waiting strategy differences, performance overhead analysis, and engineering practice scenarios.

Part 1Core Concepts: Critical Section and the Essence of Locks

Whether it is a mutex or a spin lock, the core goal is toensure that only one thread enters the critical section at any given time.

Bilibili C++ Interview: Differences Between Mutex and Spin Lock, and Their Use Cases

Therefore, before delving into the implementation of locks, it is essential to clarify:

1.1 What is a Critical Section?

A critical section refers to a code segment that requires atomic access, typicallywrapped bylock(locking) andunlock(unlocking) operations.For example:

int shared_counter = 0;
// Thread A operation shared_counter++; 
// Thread B operation shared_counter--;

The above<span><span>shared_counter++</span></span> and <span><span>shared_counter--</span></span> are the critical sections. Without a synchronization mechanism, both threads may modify<span><span>shared_counter</span></span>, leading to data inconsistency (e.g., lost updates).

1.2 The Essence of Locks

The core function of a lock ismutual exclusion: only one thread is allowed to enter the critical section at any given time. When thread T1 attempts to acquire a lock held by T2, T1 must enter a waiting state until T2 releases the lock.

Part 2Mutex (Mutual Exclusion Lock)

The core logic of a mutex is:If the lock is occupied, the current thread will enter kernel mode blocking (giving up the CPU) until the lock is released and the thread is awakened.This implementation relies on the Linux futex(Fast Userspace Mutex) mechanism, balancing user mode efficiency and kernel mode blocking capability.

2.1 Two Core Components of Mutex

1) Lock Identifier: Typically represented by an integer field indicating the state (for example, glibc’s pthread_mutex_t as an example):

  • 0: Lock is not occupied, can be acquired directly;
  • 1: Lock is occupied, but no threads are waiting;
  • 2: Lock is occupied, and there are threads waiting in the kernel.

2) Waiting Mechanism: relies onfutexsystem calls to implement a “user mode check + kernel mode blocking” hybrid logic, avoiding meaningless CPU spinning.

2.2 Key Operations: Implementation Process of Lock and Unlock

1) Lock (locking) operation

User mode atomic check: Throughcmpxchg(compare and swap) instruction atomically checks whether the lock identifier is0:

  • If it is0: Directly set the lock identifier to1, lock successful, no need to enter kernel;
  • If it is not0: Indicates that the lock is occupied, and the thread needs to enter kernel waiting.

Kernel mode blocking: Callfutex_waitsystem call to add the current thread to the futex waiting queue and switch from “running state” to “blocked state” (giving up the CPU).

2) Unlock (unlocking) operation

User mode atomic update: Throughfetch_sub instruction atomically reduces the lock identifierby1, and checks the state before the update:

  • If the state before the updatewas1: Indicates that no threads are waiting, unlock is complete, no need to enter kernel;
  • If the state before the updatewas2: Indicates that there are threads waiting, need to enter kernel to wake up.

Kernel mode wake-up: Callfutex_wakesystem call to wake up a blocked thread from the current lock’s corresponding futex waiting queue (the thread switches from “blocked state” to “ready state”).

2.3 Underlying Support for the Futex Mechanism

The Linux kernel maintains aglobal futex hash table, with the core logic as follows:

  • Each mutex corresponds to afutex key(generated from the lock’s virtual address, process ID, etc.);
  • futex keyis hashed to a slot in the hash table, with each slot associated with a “waiting thread linked list”;
  • futex_wait: Inserts the current thread into the linked list and switches to the blocked state;
  • futex_wake: Takes one thread from the linked list and switches it to the ready state.

2.4 Example of Using Mutex (C Language, Based on pthread)

#include &lt;stdio.h&gt;#include &lt;pthread.h&gt;#include &lt;unistd.h&gt;// Global critical resource (needs protection)int g_counter = 0;// Mutex (default attributes, non-adaptive mode) pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;// Thread function: increment the counter 1000 times (critical section)void* thread_func(void* arg) {    for (int i = 0; i &lt; 1000; i++) {        // Lock: if the lock is occupied, the thread will block (enter kernel waiting)        pthread_mutex_lock(&amp;g_mutex);
        // Critical section: atomically modify the counter        g_counter++;        printf("Thread %ld: counter = %d\n", (long)arg, g_counter);
        // Unlock: if there are threads waiting, wake one        pthread_mutex_unlock(&amp;g_mutex);
        // Simulate other operations outside the critical section (not necessary)        usleep(1);    }    return NULL;}
int main() {    pthread_t tid1, tid2, tid3;
    // Create 3 threads competing for the lock    pthread_create(&amp;tid1, NULL, thread_func, (void*)1);    pthread_create(&amp;tid2, NULL, thread_func, (void*)2);    pthread_create(&amp;tid3, NULL, thread_func, (void*)3);
    // Wait for threads to finish    pthread_join(tid1, NULL);    pthread_join(tid2, NULL);    pthread_join(tid3, NULL);
    // Destroy mutex    pthread_mutex_destroy(&amp;g_mutex);    return 0;}

Code Explanation: 3 threads use pthread_mutex_lock/unlock to protect the accumulation operation of g_counter. If a thread fails to acquire the lock, it will enter kernel blocking through futex, avoiding CPU spinning.

Part 3Spin Lock

The core logic of a spin lock is:when the lock is occupied, the thread loops in user mode checking the lock status (spinning) until it acquires the lock. It does not rely on kernel scheduling and waits entirely through CPU spinning.

Typical implementations of spin locks include multi-stage waiting strategies:

  • Fast Attempt: Try to acquire the lock several times (e.g., 5 times), each time approximately 25 nanoseconds
  • Delayed Attempt: Try more times and add CPU wait instructions (e.g.,<span><span>__asm__ __volatile__("pause")</span></span>), approximately 400 nanoseconds
  • Gradual CPU Yield: Loop multiple times, each time calling<span><span>sched_yield()</span></span>, approximately 200-300 nanoseconds
  • Final CPU Yield: If unable to acquire the lock for a long time, call<span><span>sched_yield()</span></span> to yield execution

3.1 Lock State Identifier

Spin locks typically use<span><span>atomic_flag</span></span> (C++11) or<span><span>std::atomic<bool></span></span> (C++ standard library atomic types) to represent the state, ensuring atomicity of operations. For example:

std::atomic_flag flag = ATOMIC_FLAG_INIT; // Initialized to unlocked state (ATOMIC_FLAG_INIT ensures initial state is false)

3.2 Locking Process

The locking logic of a spin lock typically adoptsmulti-stage waiting strategies to balance delay and CPU usage:

  • Fast Spin Phase: High-frequency attempts to acquire the lock for a short time (e.g., 5 times) without delay, suitable for scenarios where the lock is about to be released.

  • Moderate Delay Spin Phase: Introduce a brief delay (e.g., calling _mm_pause()) to reduce CPU pipeline flushing and lower power consumption.
  • Long Delay Spin Phase: Further extend the delay (e.g., calling std::this_thread::yield() to yield CPU time slice), avoiding long-term CPU occupation.
  • Final Blocking Phase (optional): If unable to acquire the lock for a long time, some implementations may degrade to kernel mode blocking (but strictly speaking, this does not count as a pure spin lock).

3.3 Unlocking Process

Set the lock state to unlocked through atomic operations (e.g.,<span><span>flag.clear()</span></span>), ensuring that other threads can perceive the release of the lock.

3.4 Typical Code Example of Spin Lock (C++ Implementation)

The following is a spin lock implementation that combines multi-stage waiting strategies, simulating optimization logic in audio and video frameworks:

#include &lt;atomic&gt;#include &lt;thread&gt;#include &lt;chrono&gt;
class SpinLock {private:    std::atomic_flag flag = ATOMIC_FLAG_INIT; // Initialized to unlocked state
public:    void lock() {        // Phase 1: Fast spin (5 times, about 25ns/time)        for (int i = 0; i &lt; 5; ++i) {            if (!flag.test_and_set(std::memory_order_acquire)) { // Atomic operation: acquire lock                return;            }        }
        // Phase 2: Moderate delay spin (10 times, 20ns pause each)        for (int i = 0; i &lt; 10; ++i) {            if (!flag.test_and_set(std::memory_order_acquire)) {                return;            }            _mm_pause(); // x86 architecture specific instruction, reduces CPU pipeline flushing        }
        // Phase 3: Long delay spin (3000 times, about 0.067ms each)        for (int i = 0; i &lt; 3000; ++i) {            if (!flag.test_and_set(std::memory_order_acquire)) {                return;            }            std::this_thread::yield(); // Yield CPU time slice (switch to ready state)        }
        // Final phase: Continuous spinning (to prevent kernel switching overhead)        while (flag.test_and_set(std::memory_order_acquire)) {            // More refined delay strategies can be added (e.g., exponential backoff)        }    }
    void unlock() {        flag.clear(std::memory_order_release); // Atomic operation: release lock    }};

Key Parameter Explanation:

  • _mm_pause(): x86 architecture CPU instruction that causes the current core to pause execution for 1 clock cycle, avoiding pipeline flushing caused by frequent test_and_set operations, reducing CPU power consumption and heat.

  • std::this_thread::yield(): Notifies the operating system that the current thread is willing to yield its CPU time slice, and the scheduler may switch to execute other threads (thread state changes from running to ready).

3.5 Differences in Waiting Strategies of Spin Locks

The core difference between spin locks and mutexes isthat they do not yield the CPU while waiting:

  • Mutex: When the lock is occupied, the thread enters kernel blocking (running state → blocked state), allowing the CPU to schedule other threads.
  • Spin Lock: When the lock is occupied, the thread continuously loops in user mode checking the lock status (running state → running state), monopolizing the CPU.

Part 4Core Differences Between Mutex and Spin Lock

Feature Mutex Spin Lock
Waiting Strategy Enter kernel waiting (blocked sleep) User mode busy waiting
Thread State Change From running state → blocked state → ready state Remains in running state (busy waiting)
Context Switch Overhead Yes (2 times: sleep and wake-up) No
CPU Utilization Low (can schedule other threads while waiting) High (busy waiting occupies CPU)
Applicable Scenarios Long critical section execution time (>1ms) Short critical section execution time (<1ms)
Implementation Complexity Lower (depends on kernel futex) Higher (requires custom waiting strategy)
Single-Core Risk No (thread switches during blocking) Possible deadlock (the thread holding the lock cannot run, and the spinning thread waits forever)
Multi-Core Adaptability Average (other cores can execute during blocking) Excellent (when multiple cores are idle, spinning waiting is efficient)

Performance Overhead Empirical Comparison

Assuming the critical section execution time is<span><span>T</span></span>, and the number of lock contention is<span><span>N</span></span>, the total overhead can be simplified as:

  • Mutex: N × (user mode check time + kernel mode switch time)
  • Spin Lock: N × T_spin (T_spin is the total spinning time)

Through<span><span>perf</span></span> tool empirical measurement (Linux environment, Intel i7-12700H):

  • When T=1ms (long critical section): The total overhead of mutex is about 100ns + 5μs (kernel switch), while the total overhead of spin lock is about 1ms (pure CPU spinning), making mutex 50 times faster.
  • When T=0.1ms (short critical section): The total overhead of mutex is about 100ns + 5μs (kernel switch dominates), while the total overhead of spin lock is about 100ns (spinning time can be ignored), making spin lock 50 times faster.

Part 5How to Choose Lock Type?

5.1 Scenarios Preferably Choosing Mutex

  • Critical sections include I/O operations (e.g., file read/write, network requests): I/O times are usually much greater than 1ms, and spin locks would waste CPU spinning.

  • Critical sections are CPU-intensive but have long execution times (e.g., matrix operations, big data processing): Spin locks would continuously occupy the CPU, affecting the scheduling of other tasks.

  • Single-core systems: Spin locks may lead to deadlocks (the thread holding the lock cannot run, and the spinning thread waits forever).

5.2 Scenarios Considering Spin Locks

  • Critical sections are extremely short and have no I/O (e.g., modifying atomic variables, updating pointers): Execution time is usually <1ms, and spin locks avoid context switch overhead.

  • Multi-core systems with low thread contention: When other cores are idle, spinning waiting is more efficient than kernel switching.

  • Real-time systems: Require extremely low latency (e.g., game engines, high-frequency trading), spin locks can avoid the uncertainty of kernel scheduling.

5.3 Advanced Optimization: Adaptive Mutex

glibc’s<span><span>pthread_mutex</span></span> supportsadaptive mode (enabled by default), combining the advantages of mutexes and spin locks:

When a thread attempts to acquire a lock, it first spins in user mode for about 4μs (by looping calling<span><span>_mm_pause()</span></span>), and if the lock is not released during the spin, it enters kernel blocking.

Implementation Logic (simplified from glibc source code):

// Pseudo code: Adaptive logic of pthread_mutex_lockvoid pthread_mutex_lock(pthread_mutex_t *mutex) {    if (mutex-&gt;lock == 0 &amp;&amp; atomic_compare_exchange_weak(&amp;mutex-&gt;lock, 0, 1)) {        return; // Quickly acquire lock    }
    int spin_count = 0;    while (spin_count &lt; MAX_SPIN) { // MAX_SPIN≈100 times (about 4μs)        if (mutex-&gt;lock == 0 &amp;&amp; atomic_compare_exchange_weak(&amp;mutex-&gt;lock, 0, 1)) {            return; // Acquire lock during spin        }        _mm_pause();        spin_count++;    }
    // Spin failed, enter kernel blocking    futex_wait(&amp;mutex-&gt;lock, 1);}

Advantages: Avoids context switch overhead during short-term blocking while preventing CPU spinning waste during long-term.

Part 6High-Frequency Interview Questions

Q1: Why can spin locks lead to deadlocks in single-core systems?

In a single-core system, if thread T1 holds the lock and enters the kernel (e.g., calls<span><span>sleep()</span></span>), while thread T2 attempts to acquire the lock and starts spinning. However, since a single core can only run one thread, T1 cannot be scheduled to run to release the lock, and T2 will spin forever, leading to a deadlock.

Q2: What is the difference between spin lock’s<span><span>yield()</span></span> and mutex’s<span><span>futex_wait()</span></span>?

  • yield(): The thread transitions from running state to ready state, and it may still be scheduled for execution by the CPU (spinning).

  • futex_wait(): The thread transitions from running state to blocked state and must wait for the kernel to wake it up to run again (does not occupy CPU).

Q3: What is the principle of adaptive mutex?

An adaptive mutex is a hybrid lock mechanism that combinesuser mode spinning andkernel mode blocking, with the core goal of avoiding context switch overhead in short critical sections and preventing CPU spinning waste in long critical sections. Its core logic isdynamically determining the lock holding time and automatically selecting the optimal waiting strategy.

Working Principle (using glibc’s pthread_mutex as an example):
  • Fast Attempt Phase: When a thread first attempts to acquire the lock, it directly checks the lock state through atomic operations (e.g., CAS). If the lock is not occupied (lock == 0), it immediately acquires the lock (no contention scenario).

  • User Mode Spinning Phase: If the lock is occupied, the thread enters a user mode spinning loop (instead of immediately entering the kernel). The number of spins is dynamically adjusted by the kernel (usually based on historical experience, such as default spinning for about 4μs).

  • During spinning, the thread reduces CPU power consumption by using instructions like _mm_pause() to avoid excessive CPU resource occupation.

  • Kernel Mode Blocking Phase: If spinning times out and the lock is still not acquired, the thread calls futex_wait to enter the kernel, adding itself to the waiting queue and suspending (from running state → blocked state). At this point, the CPU can schedule other threads for execution.

  • Wake-up and Retry: When the lock holder releases the lock (calls unlock), the kernel wakes up one waiting thread through futex_wake. The awakened thread attempts to acquire the lock again; if successful, it continues execution; if it fails (the lock has been preempted by another thread), it repeats the above process.

Q4: What is the core design of the futex mechanism? Why is it said to be an efficient collaboration between user mode and kernel mode?

Futex (Fast Userspace Mutex) is a hybrid synchronization primitive designed by the Linux kernel to optimize user mode lock performance, with the core idea being “if it can be solved in user mode, do not enter the kernel.” Its core design includes the following two points:

1. Dual-mode waiting queue (user mode + kernel mode)
  • No contention in user mode: When the lock is not contended (only one thread attempts to acquire the lock), all operations are completed in user mode (directly checking through atomic variable lock), without kernel intervention.

  • Contention in kernel mode: When the lock is contended by multiple threads (the second and subsequent threads attempt to acquire the lock), the threads are added to the kernel waiting queue through the futex_wait system call and suspended (blocked state).

2. Hash table management based on memory addresses

The kernel manages all futex instances through aglobal futex hash table:

  • Each futex instance corresponds to a unique futex key (generated from the lock’s memory address).

  • Each slot in the hash table stores a waiting thread queue (linked list structure), with threads in the queue waiting for the lock corresponding to that futex key.

Key to Efficient Collaboration:
  • Reduce system call frequency: When there is no contention, all processing is done in user mode, avoiding frequent entry into the kernel (traditional locks require a system call for each lock/unlock).

  • Precise wake-up: futex_wake only wakes up one thread in the waiting queue (not all), reducing unnecessary context switches.

Q5: Why are spin locks not suitable for single-core systems? How to avoid deadlocks?

In single-core systems, spin locks may lead todeadlocks, the core reason being “the thread holding the lock cannot be scheduled to run”.

Deadlock Scenario Analysis:

Assuming there are two threads T1 and T2 in a single-core system sharing lock L:

  • T1 acquires lock L and enters the critical section (e.g., calls sleep() to enter kernel blocking).

  • T2 attempts to acquire lock L, finds it occupied, and starts spinning (user mode looping checking lock status).

  • Since a single-core system can only run one thread, T1 is blocked in sleep() (kernel mode) and cannot be scheduled back to user mode to release lock L.

  • T2 continues to spin, unable to acquire lock L, leading to a deadlock.

Methods to Avoid:
  • Prohibit the use of spin locks in single-core systems: This is the most direct solution (e.g., single-core spin locks can be disabled in Linux kernel configuration).

  • Limit spinning time: Set a maximum number of spins (e.g., 1000 times), and after timeout, actively release the lock or downgrade to a mutex.

  • Avoid blocking while holding the lock: In the critical section protected by the spin lock, prohibit any operations that may cause the thread to block (e.g., I/O, sleep(), wait()).

Q6: How does futex avoid the “Thundering Herd” effect?

The Thundering Herd effect refers to multiple threads being awakened simultaneously, competing for the same resource, leading to a large number of ineffective context switches. Futex avoids this problem throughprecise wake-up mechanisms.

Traditional Scenario of Thundering Herd Effect:

In traditional multithreaded synchronization (e.g., semaphore), when a resource becomes available, the kernel may wake all waiting threads, causing them to compete for the resource simultaneously, ultimately only one thread succeeds in acquiring it, while the others block again. This “collective wake-up” wastes a significant amount of CPU resources.

Futex’s Solution:

The futex’s<span><span>futex_wake</span></span> operation supportsspecifying the number of threads to wake up (e.g.,<span><span>futex_wake(&lock, 1)</span></span> means only waking up 1 thread). The kernel selects one thread to wake up from the waiting queue based on priority (e.g., FIFO or fair policy), while the remaining threads continue to wait. This way:

  • Only 1 thread is awakened and attempts to acquire the lock, avoiding a large number of threads competing simultaneously.

  • The threads that are not awakened remain in the blocked state and do not occupy CPU resources.

Q7: How is the fairness of spin locks guaranteed? Is there a problem of thread starvation?

The fairness of spin locks (i.e., whether the order of thread acquisition of the lock conforms to the waiting order) depends on the specific implementation. By default, spin locksdo not guarantee fairness, which may lead to thread starvation.

Fairness Issue Analysis:
  • Unfair Spin Locks: The order of thread acquisition of the lock is unrelated to the waiting order. For example, thread T1 starts spinning first, and thread T2 starts spinning later, but T2 may acquire the lock first due to more timely CPU scheduling, leading to T1 starving.

  • Fair Spin Locks: By using additional queue structures (e.g., MCS locks, CLH locks) to record the order of waiting threads, ensuring that threads that wait first acquire the lock first. However, fair locks increase memory overhead (need to maintain a queue) and delay (need to wake up in order).

Common Implementation Choices:
  • Spin locks in the kernel (e.g., Linux kernel spin locks) are usually unfair locks, as the kernel focuses more on throughput (reducing context switches) rather than strict fairness.

  • User mode spin locks (e.g., C++’s std::mutex on some platforms) may combine fair strategies but need to be explicitly enabled (e.g., through PTHREAD_MUTEX_ADAPTIVE_NP attribute).

Conclusion

The core essence is that the differences between mutexes and spin locks stem from the “waiting strategy”—blocking vs. spinning, which in turn determines the overhead and applicable scenarios;Selection Principle: Use spin locks (or atomic operations) for short critical sections (<1ms) and mutexes for long critical sections (>1ms);Engineering Advice: Prefer using mutexes, only attempt spin locks when performance bottlenecks are clear; use spin locks in kernel mode, and be cautious in user mode;Interview Focus: Principles of adaptive mutexes, futex mechanisms, and the role of_mm_pause are high-frequency exam points.

Previous Recommendations

NVIDIA C++ Tegra Interview: What is the underlying principle of mutex?

Xiaomi C++ Campus Recruitment Second Interview: What are the differences between epoll, poll, and select?

ByteDance Second Interview: For performance, would you sacrifice the three paradigms of databases?

Click below to follow 【Linux Tutorials】 to get programming learning routes, project tutorials, resume templates, large factory interview question PDFs, large factory interview experiences, programming communication circles, etc.

Leave a Comment