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)进行售票操作:
#
ticket = ;
* {
*id = (*)arg;
() {
(ticket > ) {
usleep();
(, id, ticket);
ticket--;
} {
;
}
}
;
}
{
t1, t2, t3, t4;
pthread_create(&t1, , route, (*));
pthread_create(&t2, , route, (*));
pthread_create(&t3, , route, (*));
pthread_create(&t4, , route, (*));
pthread_join(t1, );
pthread_join(t2, );
pthread_join(t3, );
pthread_join(t4, );
;
}


