互斥锁(Mutex, Mutual Exclusion Lock)是一种用于保护共享资源,确保在任意时刻只有一个线程可以访问该资源的同步原语。其核心目的是解决多线程环境下的**数据竞争(Data Race)**问题,防止因并发访问导致的数据不一致 。

1. 核心原理与工作模型

互斥锁的基本原理是:在访问共享资源前,线程必须先获取(Acquire/Lock)锁;访问结束后,必须释放(Release/Unlock)锁。如果锁已被其他线程持有,则试图获取锁的线程会被阻塞(Block),直到锁被释放。

其工作模型可以类比为一个只有一个房间的卫生间

  • 锁(Lock):卫生间的门锁。
  • 获取锁:进入卫生间并锁门。
  • 共享资源:卫生间内部。
  • 释放锁:使用完毕,开门离开。
  • 阻塞:其他人在门外等待。

从底层实现看,互斥锁通常依赖于操作系统的原子指令和内核对象。一个简化的实现可能包含两个关键状态和一个等待队列:

// 伪代码示意互斥锁结构
struct mutex {
    int locked;          // 锁状态:0-未锁定, 1-已锁定
    thread_t *owner;     // 当前持有锁的线程
    wait_queue_t waiters; // 等待该锁的线程队列
};
  • 加锁操作:通过原子操作(如 test-and-setcompare-and-swap)尝试将 locked 从 0 改为 1。若成功,则设置 owner 为当前线程并返回;若失败(锁已被持有),则当前线程被放入 waiters 队列并进入睡眠状态 。
  • 解锁操作:将 locked 置为 0,owner 置为 NULL,然后从 waiters 队列中唤醒一个等待线程 。

2. 使用方法与代码示例

不同编程语言和平台提供了各自的互斥锁API。下面以 POSIX 线程(pthread)库和 C++ 标准库为例。

2.1 POSIX Threads (pthread) 示例

POSIX 线程库是 Linux/Unix 系统上多线程编程的标准。

#include <pthread.h>
#include <stdio.h>

// 共享资源
int shared_counter = 0;
// 定义并初始化一个互斥锁
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void* increment_counter(void* arg) {
    for (int i = 0; i < 100000; ++i) {
        // 进入临界区前加锁
        pthread_mutex_lock(&counter_mutex);
        
        // 临界区:访问共享资源
        shared_counter++;
        
        // 离开临界区后解锁
        pthread_mutex_unlock(&counter_mutex);
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    
    // 创建两个线程并发执行 increment_counter
    pthread_create(&thread1, NULL, increment_counter, NULL);
    pthread_create(&thread2, NULL, increment_counter, NULL);
    
    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    // 销毁互斥锁(对于静态初始化的锁,通常可省略,但动态初始化需销毁)
    // pthread_mutex_destroy(&counter_mutex);
    
    printf("Final counter value: %d (Expected: 200000)
", shared_counter);
    return 0;
}
  • pthread_mutex_lock(&mutex):获取锁。若锁已被占用,则调用线程阻塞 。
  • pthread_mutex_unlock(&mutex):释放锁 。
  • PTHREAD_MUTEX_INITIALIZER:用于静态初始化互斥锁的宏 。对于动态初始化,需使用 pthread_mutex_init()pthread_mutex_destroy()

2.2 C++ std::mutex 示例

C++11 在标准库中引入了 <mutex> 头文件,提供了跨平台的互斥锁支持。

#include <iostream>
#include <thread>
#include <mutex>

int shared_counter = 0;
std::mutex counter_mutex; // 定义互斥锁对象

void increment_counter() {
    for (int i = 0; i < 100000; ++i) {
        // 使用 std::lock_guard 自动管理锁的生命周期
        std::lock_guard<std::mutex> lock(counter_mutex);
        // lock_guard 构造函数中调用 mutex.lock(),析构时调用 mutex.unlock()
        shared_counter++;
        // 离开作用域,lock_guard 析构,自动释放锁
    }
}

int main() {
    std::thread thread1(increment_counter);
    std::thread thread2(increment_counter);
    
    thread1.join();
    thread2.join();
    
    std::cout << "Final counter value: " << shared_counter << " (Expected: 200000)" << std::endl;
    return 0;
}
  • std::mutex:基本的互斥锁类 。
  • std::lock_guard:RAII(资源获取即初始化)风格的锁管理模板。它在构造时自动加锁,析构时自动解锁,有效避免了因异常或忘记调用 unlock() 导致的死锁 。

3. 互斥锁的典型应用场景

互斥锁适用于任何需要互斥访问共享资源的场景。下表对比了不同场景下互斥锁的使用:

应用场景 描述 示例
保护共享变量 防止多个线程同时读写同一个全局变量或对象成员。 多线程计数器、状态标志、配置数据等。
保护数据结构 确保对链表、队列、哈希表等复杂数据结构的操作是原子的。 多线程生产-消费者模型中的任务队列 。
保护临界区代码 确保一段代码(临界区)在同一时间只被一个线程执行。 访问文件、操作硬件等非线程安全的函数调用序列。
实现更高级的同步 作为构建条件变量、信号量等更复杂同步机制的基础。 条件变量 (pthread_cond_t) 必须与一个互斥锁配合使用 。

4. 死锁问题及其预防

死锁(Deadlock)是多线程编程中一个严重的问题,指两个或更多线程因互相等待对方持有的资源而无限期阻塞的状态 。产生死锁通常需要同时满足四个必要条件:

  1. 互斥条件:资源一次只能被一个线程占用。
  2. 请求与保持条件:线程在持有至少一个资源的同时,请求新的资源。
  3. 不可剥夺条件:线程已获得的资源在未使用完之前不能被强行抢占。
  4. 循环等待条件:存在一个线程-资源的环形等待链 。

预防死锁的策略:

  1. 破坏“请求与保持”条件:一次性申请所有所需资源,如果无法全部获取,则释放已持有资源并等待。这类似于数据库中的两阶段锁协议,但可能降低并发度 。
  2. 破坏“不可剥夺”条件:允许系统在必要时强行剥夺线程已持有的资源。但这实现复杂,且可能引发回滚等额外开销。
  3. 破坏“循环等待”条件对资源进行全局排序,规定所有线程必须按顺序申请资源。这是实践中最常用且有效的预防策略 。
// 示例:通过规定锁的获取顺序来预防死锁
std::mutex mutex_a, mutex_b;

void safe_func1() {
    // 规定先锁 mutex_a,再锁 mutex_b
    std::lock_guard<std::mutex> lock_a(mutex_a);
    std::lock_guard<std::mutex> lock_b(mutex_b);
    // 操作共享资源...
}

void safe_func2() {
    // 同样遵守先A后B的顺序,即使它可能不需要先访问A保护的数据
    std::lock_guard<std::mutex> lock_a(mutex_a);
    std::lock_guard<std::mutex> lock_b(mutex_b);
    // 操作共享资源...
}
// 如果所有线程都遵守同一顺序,就不可能形成循环等待。
  1. 使用标准库工具:C++17 提供了 std::scoped_lock,可以一次性锁定多个互斥量,且内部采用避免死锁的算法(如 std::lock 的活锁避免算法),是处理多个锁时的更佳选择 。
  2. 银行家算法:一种动态避免死锁的算法,系统在分配资源前先计算安全性,仅当分配后系统仍处于安全状态时才进行分配 。该算法更多用于操作系统理论教学,在通用应用程序编程中较少直接实现。

5. 与其他同步机制的对比

互斥锁是基础的同步原语,常与条件变量、信号量等结合使用。下表简要对比了几种常见同步机制:

机制 核心目的 与互斥锁的关键区别
互斥锁 (Mutex) 互斥访问共享资源。 二元状态(锁定/未锁定),由持有锁的线程释放。
条件变量 (Condition Variable) 等待特定条件成立,常与互斥锁配合使用。 本身不保护资源,用于线程间的通知。线程在等待条件时会释放关联的互斥锁,被唤醒后重新获取锁 。
信号量 (Semaphore) 控制访问共享资源的线程数量(允许多个)。 是一个计数器,可以初始化为大于1的值,用于控制并发访问的许可数量,不关心持有者身份 。
自旋锁 (Spinlock) 互斥访问,但在获取锁失败时忙等待(循环检查)。 适用于临界区极短、且线程阻塞切换开销大于忙等待开销的场景(如内核开发)。用户态编程通常优先使用会阻塞线程的互斥锁。

总结:互斥锁是多线程编程中防止数据竞争、保证原子操作的基石。正确使用互斥锁需要遵循“加锁-访问-解锁”的模式,并警惕死锁风险。通过结合 RAII 管理类(如 lock_guard)、规定锁的获取顺序以及使用更高级的同步抽象,可以构建出既安全又高效的多线程程序。


参考来源

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐