Linux 系统编程:线程互斥原理与实战指南
在多线程编程的世界里,线程互斥是绕不开的核心知识点。想象一下,多个线程同时操作同一个售票系统的余票变量,结果可能卖出负数的车票;多个线程同时修改同一个全局变量,最终的结果可能和预期天差地别。这些都是多线程并发访问共享资源引发的数据竞争问题,而线程互斥就是解决这类问题的关键。
本文将从核心概念出发,拆解临界资源、临界区、原子性等基础知识点,深入 Linux 下互斥量(mutex)的使用与实现原理,并通过实战案例完成从理论到实践的落地,最后讲解 C++ 下的 RAII 风格锁封装,让你不仅懂原理,还能写出优雅、安全的多线程代码。
核心概念:共享资源与临界区
在学习具体操作之前,先把几个核心概念吃透,这是理解后续所有内容的基础。
共享资源与临界资源
在多线程程序中,线程之间可以通过共享数据完成交互,这些被多个线程共同访问的数据就是共享资源。比如售票系统中的余票变量 ticket、电商系统中的库存变量 stock,都是典型的共享资源。
但并不是所有共享资源都需要特殊保护,需要被保护的共享资源才是临界资源。简单来说,临界资源是多线程执行流中,不能被多个线程同时访问的资源,一旦同时访问,就会引发数据不一致、结果异常等问题。
举个例子,售票系统的 ticket 变量是临界资源,因为多个售票线程同时对它进行'判断是否大于 0→打印余票→减 1'的操作时,会出现计算错误;而每个线程内部的局部变量只归当前线程所有,其他线程无法访问,就不是临界资源,无需保护。
临界区
有了临界资源,就对应有临界区。每个线程内部,访问临界资源的代码段,就是临界区。非临界区则是线程中不访问临界资源的代码,多个线程可以并发执行,无需限制。
比如售票线程中的这段代码,就是典型的临界区:
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
这段代码直接访问了临界资源 ticket,是需要被互斥保护的核心代码段;而线程中其他的打印日志、局部变量计算等代码,都属于非临界区。
互斥的定义
搞懂了临界资源和临界区,就可以定义互斥了:任何时刻,保证有且只有一个执行流进入临界区,访问临界资源,这就是互斥。互斥的核心目的是对临界资源进行保护,避免多个线程同时操作临界资源引发的数据竞争问题。
原子性:互斥的底层要求
要实现有效的互斥,操作临界资源的代码必须满足原子性。原子性指的是不会被任何调度机制打断的操作,该操作只有两种状态:要么完成,要么未完成,不存在'执行了一半'的中间状态。
在多线程环境中,操作系统的线程调度是随机的(时间片轮转),如果一个操作不具备原子性,执行到一半时被其他线程抢占 CPU,就会导致临界资源的状态混乱。这也是多线程操作共享资源出问题的根本原因——对临界资源的操作不是原子操作。
先看个反面案例:多线程售票
光说概念不够直观,我们直接写一个经典的多线程售票系统案例,看看多个线程并发操作临界资源时,会出现什么样的问题。
问题代码:未加互斥的售票系统
我们创建 4 个售票线程,同时对全局变量 ticket(初始值 100)进行售票操作:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 临界资源:售票系统余票
int ticket = 100;
// 售票线程执行函数
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
// 临界区:访问临界资源 ticket
if (ticket > 0) {
// 模拟售票的耗时业务(比如联网查询、打印车票)
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
return NULL;
}
int main(void) {
pthread_t t1, t2, t3, t4;
// 创建 4 个售票线程
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
// 等待线程执行完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
编译运行与异常结果
将上述代码保存为 ticket.c,使用 gcc 编译(需要链接 pthread 库):
gcc ticket.c -o ticket -lpthread
./ticket
运行后会发现异常结果:余票会出现 0、-1、-2 等情况,比如:
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
明明我们在代码中判断了 ticket > 0 才会售票,为什么会卖出负数的车票?这就是多线程并发访问临界资源的典型问题,我们来一步步拆解原因。
问题根源:三步分析
出现负数车票的原因主要有三点,层层递进,也是所有多线程共享资源问题的共性原因:
1. 线程调度的随机性
if (ticket > 0) 判断条件为真后,操作系统可能会将当前线程的 CPU 时间片收回,切换到其他线程。比如线程 1 判断 ticket=1 为真,还没执行 ticket--,就被切走了;此时线程 2、3、4 也判断 ticket=1 为真,都进入了售票逻辑,最终多个线程对 ticket 进行减 1 操作,就出现了负数。
2. 耗时操作放大了竞争问题
代码中的 usleep(1000) 模拟了售票的耗时业务,这让线程在临界区中停留的时间变长,线程调度的概率大大增加,使得数据竞争的问题更明显。即使去掉 usleep,由于线程调度的随机性,依然会出现问题,只是概率降低。
3. ticket--本身不是原子操作
这是最根本的原因:我们看似简单的 ticket-- 操作,在计算机底层并不是一条指令,而是对应三条汇编指令:
load:将共享变量
ticket从内存加载到 CPU 寄存器中; update:在寄存器中执行减 1 操作,更新值; store:将寄存器中的新值写回ticket的内存地址。
我们可以通过 objdump 命令查看 ticket-- 的汇编代码来验证这一点。三条汇编指令意味着 ticket-- 可以被线程调度打断,执行到一半时被其他线程抢占,导致临界资源的状态混乱。
解决问题的核心要求
要解决上述问题,我们需要让临界区的代码满足三个核心要求,这也是设计互斥锁的基本准则:
互斥行为:当一个线程进入临界区执行时,不允许其他线程进入该临界区; 唯一准入:如果多个线程同时请求进入临界区,且临界区无线程执行,只能允许一个线程进入; 非阻塞无关:如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要满足这三个要求,本质上就是需要一把锁——在 Linux 下,这把锁就是互斥量(mutex)。
Linux 下的互斥量:mutex 的使用全解析
Linux 提供了一套完整的 POSIX 线程库(pthread),其中的**互斥量(pthread_mutex_t)**是实现线程互斥的核心工具。互斥量就像临界区的'大门钥匙',只有拿到钥匙的线程才能进入临界区,访问临界资源;其他线程只能等待,直到钥匙被归还。
接下来我们详细讲解互斥量的初始化、销毁、加锁、解锁等核心接口,以及如何用互斥量改造售票系统,解决数据竞争问题。
互斥量的类型与核心接口
互斥量的核心类型是 pthread_mutex_t,所有操作都围绕这个类型展开,核心接口包括初始化、销毁、加锁、解锁,均在 <pthread.h> 头文件中声明。
初始化互斥量
互斥量的初始化有两种方式:静态分配和动态分配,适用于不同的场景。
方式 1:静态分配
如果互斥量是全局变量或静态变量,可以直接使用宏 PTHREAD_MUTEX_INITIALIZER 初始化,简单高效,无需手动调用函数:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
方式 2:动态分配
如果互斥量是局部变量或通过 malloc 在堆上分配的,需要使用 pthread_mutex_init 函数初始化:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
mutex:指向要初始化的互斥量对象的指针;attr:互斥量的属性,一般设为NULL(使用默认属性)。 返回值:成功返回 0,失败返回对应的错误号。
销毁互斥量
互斥量使用完成后需要销毁,释放相关资源,核心函数是 pthread_mutex_destroy:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁互斥量的注意事项(非常重要):
静态分配的互斥量(用
PTHREAD_MUTEX_INITIALIZER初始化)不需要手动销毁; 不要销毁一个已经加锁的互斥量,否则会导致未定义行为; 已经销毁的互斥量,要确保后续没有线程再尝试加锁。
加锁与解锁
互斥量的核心操作是加锁(pthread_mutex_lock) 和解锁(pthread_mutex_unlock),这两个操作是实现互斥的关键。
加锁函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:尝试获取互斥量的锁,如果互斥量未被锁定,则成功加锁,当前线程继续执行;如果互斥量已被其他线程锁定,则当前线程会阻塞(执行流被挂起),直到互斥量被解锁。
解锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:释放互斥量的锁,将互斥量置为未锁定状态;如果有其他线程因等待该互斥量而阻塞,会唤醒其中一个线程(由操作系统调度),让其尝试获取锁。
加锁 / 解锁的注意事项:
加锁和解锁必须成对出现,加锁后忘记解锁会导致死锁; 临界区的代码要尽可能精简,加锁后尽快解锁,减少线程阻塞的时间,提高程序并发效率; 加锁和解锁的调用必须在同一个线程中,不能在 A 线程加锁,B 线程解锁。
实战改造:加互斥量的售票系统
我们用互斥量改造之前的售票系统,将临界区的代码用加锁和解锁包裹,保证同一时刻只有一个线程进入临界区操作 ticket 变量。
改造后的代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
// 全局互斥量,静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
// 加锁:进入临界区前获取锁
pthread_mutex_lock(&mutex);
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
// 解锁:临界区代码执行完成,释放锁
pthread_mutex_unlock(&mutex);
} else {
// 注意:else 分支也要解锁,否则会导致死锁
pthread_mutex_unlock(&mutex);
break;
}
// 可选:让出 CPU,让其他线程有机会执行,提高并发度
// sched_yield();
}
return NULL;
}
int main(void) {
pthread_t t1, t2, t3, t4;
// 创建 4 个售票线程
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
// 等待线程执行完成
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
// 销毁互斥量(静态初始化可省略,这里为了规范)
pthread_mutex_destroy(&mutex);
return 0;
}
关键改造点说明
定义了全局互斥量
mutex,使用静态初始化方式,简单方便; 在进入临界区前调用pthread_mutex_lock(&mutex)加锁,保证只有一个线程能进入; 临界区代码执行完成后,立即调用pthread_mutex_unlock(&mutex)解锁; else 分支必须解锁:如果ticket <= 0,线程会直接 break,若不解锁,互斥量会一直处于锁定状态,其他线程永远阻塞,导致死锁。
编译运行与正确结果
编译运行代码:
gcc ticket_mutex.c -o ticket_mutex -lpthread
./ticket_mutex
此时运行结果会完全符合预期,余票从 100 依次递减到 1,不会出现 0 或负数。
至此,我们成功通过互斥量解决了多线程售票系统的 data race 问题,实现了线程互斥。
互斥量的实现原理:为什么能保证原子性?
通过上面的案例,我们已经会用互斥量了,但作为程序员,我们需要知道互斥量本身是如何实现原子性的?毕竟互斥量的加锁操作也是对共享资源(互斥量自身的状态)的操作,如果加锁操作不是原子的,依然会出现竞争问题。
核心原理:硬件提供的原子指令
要实现互斥量的原子加锁 / 解锁,硬件层面为我们提供了原子交换指令(如 x86 架构的 xchgb 指令、ARM 架构的 swp 指令)。这些指令的特点是一条指令完成内存和寄存器的数据交换,而 CPU 的指令执行是原子的,不会被调度打断,这是互斥量实现的基础。
以 x86 的 xchgb(字节交换)指令为例,它能完成'将寄存器中的值和内存地址中的值交换'的操作,整个过程是原子的,即使在多处理器平台,访问内存的总线周期也有先后,一个处理器的交换指令执行时,另一个处理器的交换指令只能等待总线周期,保证了操作的唯一性。
互斥量的加锁 / 解锁伪代码实现
我们用伪代码模拟互斥量的 lock 和 unlock 操作,基于 xchgb 原子指令,让你更直观地理解原理(假设互斥量 mutex 的初始值为 1,1 表示未锁定,0 表示已锁定):
加锁操作(lock)
lock:
movb $0, %al ; 将寄存器 al 的值设为 0(表示锁定状态)
xchgb %al, mutex ; 原子交换:al 和 mutex 的内存值交换
if (%al == 0) {
; 交换后如果 al 为 0,说明 mutex 原本是 0(已被锁定)
挂起当前线程; ; 线程阻塞,等待解锁
goto lock; ; 被唤醒后重新尝试加锁
}
return 0; ; 交换后 al 为 1,说明加锁成功
加锁逻辑解析:
线程先将寄存器设为 0,代表'想要锁定'; 通过原子交换指令,将寄存器的值和互斥量的内存值交换; 如果交换后寄存器的值为 0,说明互斥量原本是 0(已被其他线程锁定),当前线程挂起等待; 如果交换后寄存器的值为 1,说明互斥量原本是 1(未锁定),加锁成功,线程进入临界区。
解锁操作(unlock)
unlock:
movb $1, mutex ; 将互斥量的内存值设为 1(未锁定状态),原子操作
唤醒等待 mutex 的线程 ; 唤醒因该互斥量阻塞的线程,让其重新尝试加锁
return 0;
解锁逻辑解析:
直接将互斥量的值设为 1,恢复未锁定状态(赋值操作是原子的); 唤醒等待该互斥量的线程,让这些线程重新进入加锁的竞争流程。
核心总结
互斥量的实现依赖硬件的原子指令,保证了加锁操作的原子性;而互斥量的加锁 / 解锁又保证了临界区代码的原子性,从而实现了对临界资源的保护。简单来说:硬件原子指令保证了锁的安全性,锁保证了临界区的安全性。
C++ 封装互斥量:RAII 风格让锁更安全、更优雅
在 C 语言中,我们需要手动调用 pthread_mutex_lock 和 pthread_mutex_unlock,必须保证成对出现,否则容易出现死锁。而在 C++ 中,我们可以利用RAII(资源获取即初始化) 思想,对互斥量进行封装,让锁的生命周期和对象的生命周期绑定,自动加锁、自动解锁,从根本上避免忘记解锁的问题。
RAII 的核心思想是:在对象构造时获取资源,在对象析构时释放资源。因为 C++ 的对象析构是自动的(无论正常退出还是异常退出,对象超出作用域时都会调用析构函数),所以基于 RAII 封装的锁,能保证锁一定会被释放,极大地提高了代码的安全性和优雅性。
接下来我们实现一个 C++ 的互斥量封装,包括基础的 Mutex 类和RAII 风格的 LockGuard 类,并改造售票系统为 C++ 版本。
封装互斥量:Lock.hpp 头文件
我们将封装的代码写在 Lock.hpp 头文件中,实现跨文件复用:
#pragma once
#include <iostream>
#include <pthread.h>
#include <cassert>
// 命名空间,避免命名冲突
namespace LockModule {
// 封装基础的互斥量类
class Mutex {
public:
// 禁止拷贝和赋值(互斥量不能被拷贝)
Mutex(const Mutex &) = delete;
const Mutex &operator=(const Mutex &) = delete;
// 构造函数:动态初始化互斥量
Mutex() {
int ret = pthread_mutex_init(&_mutex, nullptr);
assert(ret == 0);
(void)ret;
}
// 加锁
void Lock() {
int ret = pthread_mutex_lock(&_mutex);
assert(ret == 0);
(void)ret;
}
// 解锁
void Unlock() {
int ret = pthread_mutex_unlock(&_mutex);
assert(ret == 0);
(void)ret;
}
// 获取原生的 pthread_mutex_t 指针(供条件变量等使用)
pthread_mutex_t *GetMutexOriginal() {
return &_mutex;
}
// 析构函数:销毁互斥量
~Mutex() {
int ret = pthread_mutex_destroy(&_mutex);
assert(ret == 0);
(void)ret;
}
private:
pthread_mutex_t _mutex;
};
// RAII 风格的锁守卫类:自动加锁、自动解锁
class LockGuard {
public:
// 构造函数:传入互斥量对象,立即加锁
explicit LockGuard(Mutex &mutex) : _mutex(mutex) {
_mutex.Lock();
}
// 析构函数:对象析构时解锁
~LockGuard() {
_mutex.Unlock();
}
// 禁止拷贝和赋值
LockGuard(const LockGuard &) = delete;
const LockGuard &operator=(const LockGuard &) = delete;
private:
Mutex &_mutex;
};
}
封装代码解析
Mutex 类:基础互斥量封装
封装了原生的
pthread_mutex_t,对外提供Lock()、Unlock()、GetMutexOriginal()接口,隐藏底层实现; 禁止拷贝和赋值:互斥量是独占资源,不能被拷贝,通过= delete禁用拷贝构造函数和赋值运算符; 构造函数初始化互斥量,析构函数销毁互斥量,保证资源的正确管理; 使用assert做断言检查,调试阶段能快速发现互斥量操作的错误,release版本中assert会被屏蔽,不影响性能。
LockGuard 类:RAII 锁守卫
构造函数接收
Mutex对象的引用(避免拷贝),并立即调用_mutex.Lock()加锁; 析构函数调用_mutex.Unlock()解锁,对象超出作用域时自动执行; 同样禁止拷贝和赋值,保证锁的唯一性; 使用explicit关键字修饰构造函数,避免隐式类型转换,提高代码可读性。
C++ 版本实战:RAII 锁改造售票系统
我们用封装的 Mutex 和 LockGuard 改造售票系统,代码更简洁、更安全,无需手动调用加锁和解锁:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"
using namespace LockModule;
int ticket = 1000;
Mutex mutex;
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
// 定义 LockGuard 对象,构造时自动加锁
LockGuard lockguard(mutex);
// 临界区:无需手动加锁/解锁
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
// lockguard 对象超出作用域,析构时自动解锁
}
return nullptr;
}
int main(void) {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
// 无需手动销毁互斥量,mutex 对象析构时自动销毁
return 0;
}
代码优势分析
无需手动加锁 / 解锁:只需定义
LockGuard对象,构造时自动加锁,析构时自动解锁,即使临界区出现break、return或异常,也能保证解锁; 代码更简洁:临界区的代码无需被加锁 / 解锁函数包裹,逻辑更清晰; 彻底避免死锁:从根本上解决了'忘记解锁'的问题,这是 C++ RAII 封装的最大优势; 可复用性强:封装的Mutex和LockGuard可以在任何多线程程序中使用,无需重复编写底层代码。
补充:C++11 标准库中已经提供了原生的互斥量和 RAII 锁,分别是 std::mutex 和 std::lock_guard,用法和我们封装的类几乎一致,底层也是基于平台的互斥量实现(Linux 下是 pthread_mutex_t)。我们自己封装的目的是为了更深入地理解 RAII 的原理,实际开发中可以直接使用 C++11 的标准库。
常见问题与避坑指南
在实际开发中,使用互斥量不仅要懂原理、会用接口,还要避开常见的坑,否则容易出现死锁、性能低下、过度加锁等问题。
死锁(最严重的问题)
死锁是多线程编程中最严重的问题之一,指的是多个线程互相持有对方需要的锁,且都不释放自己的锁,导致所有线程永远阻塞。
产生条件
死锁的产生必须同时满足四个必要条件,缺一不可:
互斥条件:一个资源每次只能被一个线程使用; 请求与保持条件:线程持有已获取的锁,同时请求其他线程持有的锁,且不释放自己的锁; 不剥夺条件:线程已获取的锁,在未使用完之前,不能被其他线程强行剥夺; 循环等待条件:多个线程形成头尾相接的循环,每个线程都等待下一个线程持有的锁。
避免方法
避免死锁的核心是破坏死锁的四个必要条件之一,其中破坏循环等待条件是最常用、最易实现的方法:
统一加锁顺序:所有线程获取多个锁时,按照固定的顺序加锁; 一次性获取所有锁:使用
pthread_mutex_lock的扩展接口(或 C++11 的std::lock),一次性获取多个锁; 设置锁的超时时间:使用pthread_mutex_timedlock,如果在指定时间内未获取到锁,就放弃并释放已持有的锁; 避免嵌套加锁:尽量减少锁的嵌套使用,嵌套加锁是死锁的高发场景; 加锁后尽快解锁:减少线程持有锁的时间,降低锁竞争的概率。
过度加锁导致并发效率低下
互斥的本质是牺牲部分并发效率,保证数据安全,但如果过度加锁,会导致程序的并发效率急剧下降,甚至退化为串行执行。
优化技巧
临界区最小化:只对访问临界资源的核心代码加锁,非临界区的代码尽量放在锁外; 细粒度加锁:将大的临界资源拆分为多个小的临界资源,为每个小资源分配独立的互斥量; 避免无意义的加锁:只对临界资源的操作加锁,线程局部变量、只读共享资源无需加锁; 使用自旋锁替代互斥量:如果临界区的代码执行时间极短,可以使用自旋锁(Linux 下的
pthread_spinlock_t),自旋锁不会让线程阻塞,而是一直尝试获取锁,直到成功。
锁的拷贝与赋值
互斥量是独占资源,不能被拷贝或赋值,因为拷贝互斥量会导致多个互斥量对象指向同一个底层资源,或创建一个新的互斥量对象,破坏互斥的语义,引发未定义行为。
避坑方法
在 C++ 中,通过
= delete显式禁用互斥量类的拷贝构造函数和赋值运算符; 在 C 语言中,避免将互斥量作为函数参数值传递(值传递会拷贝),应使用指针或引用传递; 不要将互斥量放入容器中(容器的操作会涉及拷贝),如果需要,应放入容器的指针或智能指针。
在信号处理函数中使用互斥量
在 Linux 中,信号处理函数中不能使用 pthread 的互斥量,因为 pthread 的互斥量是可重入的,但信号处理函数的执行是异步的,可能会导致死锁。
避坑方法
信号处理函数中尽量只做简单的操作,比如设置标志位,不要访问临界资源; 如果需要在信号处理函数中访问临界资源,使用 Linux 提供的信号量(sem_t) 或原子变量,而不是互斥量; 避免在持有互斥量时触发信号处理函数。
应用场景
线程互斥是多线程编程的基础,几乎所有的多线程共享资源场景都需要用到线程互斥,以下是一些典型的应用场景:
售票系统 / 库存系统:多个线程同时修改余票 / 库存变量,需要互斥保护; 银行转账系统:多个线程同时操作账户余额,需要保证转账操作的原子性; 日志系统:多个线程同时向日志文件写入内容,需要保证日志的完整性,避免内容错乱; 缓存系统:多个线程同时读写缓存(如内存中的哈希表),需要互斥保护缓存数据; 生产消费模型:生产者和消费者线程同时操作阻塞队列,需要互斥保护队列的入队 / 出队操作; 线程池:多个工作线程同时从任务队列中获取任务,需要互斥保护任务队列。
简单来说,只要多个线程同时访问并修改同一个共享资源,就需要使用线程互斥。而对于只读的共享资源,无需互斥保护,因为只读操作不会改变资源的状态,不会引发数据竞争。
总结
线程互斥是多线程同步的基础,但实际开发中,除了互斥,我们还需要线程同步(让线程按照特定的顺序执行),比如生产消费模型中,生产者生产数据后,需要通知消费者消费;消费者消费完数据后,需要通知生产者生产。掌握互斥机制,是构建高可靠并发系统的基石。


