线程的互斥是多线程编程中的一个关键概念,旨在确保多个线程在访问共享资源时不会发生数据竞争或其他一致性问题。让我们详细探讨一下这个概念及其实现方式。
1. 线程的互斥
1.1 进程线程间的互斥相关背景概念
- 临界资源:多个线程共享的资源称为临界资源。
- 临界区:在每个线程内部,访问临界资源的代码段称为临界区。
- 互斥:任何时刻,互斥保证只有一个线程可以进入临界区访问临界资源,通常用于保护临界资源。
- 原子性:原子操作是指不会被任何调度机制打断的操作,操作要么完成,要么未完成。
1.2 互斥量(mutex)的基本概念
大多数情况下,线程使用的数据是局部变量,存储在线程的栈空间内,这些变量仅属于单个线程,其他线程无法访问。然而,某些变量需要在线程间共享,这些称为共享变量,通过共享变量,线程之间可以进行交互。多个线程并发操作共享变量时,可能会引发问题,因此需要互斥来确保数据的一致性。
为什么多线程之间需要互斥?
让我们通过一个实际的例子——抢票系统——来理解这个问题:
代码:
// 操作共享变量会有问题的售票系统代码 #include#include #include #include #include int ticket = 100; void *route(void *arg) { char *id = (char*)arg; while (1) { if (ticket > 0) { usleep(1000); printf("%s sells ticket:%d\n", id, ticket); ticket--; } else { break; } } } int main(void) { pthread_t t1, t2, t3, t4; pthread_create(&t1, NULL, route, "thread 1"); pthread_create(&t2, NULL, route, "thread 2"); pthread_create(&t3, NULL, route, "thread 3"); pthread_create(&t4, NULL, route, "thread 4"); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL); pthread_join(t4, NULL); }
执行结果:

在没有加锁(互斥)的代码执行中,我们发现票数竟然变成了负数,这是不可接受的。
为什么会抢到负数票?
这是因为共享资源在访问时没有被保护,且操作本身不是原子的:
-
if语句判断条件为真后,代码可能被切换到其他线程。 -
usleep模拟漫长的业务过程,在此期间多个线程可能进入该代码段。 -
--操作本身不是原子操作。
-- 操作的非原子性
-- 操作对应三条汇编指令:
-
load:将共享变量ticket从内存加载到寄存器中。 -
update:在寄存器中执行-1操作。 -
store:将新值写回共享变量ticket的内存地址。
解决方式
为了解决这个问题,需要确保三点:
- 代码进入临界区时,其他线程不能进入该临界区。
- 如果多个线程同时请求进入临界区,且临界区没有线程在执行,只能允许一个线程进入。
- 如果线程不在临界区执行,不能阻止其他线程进入临界区。
这三点可以通过使用互斥锁(mutex)来实现。

2. 三种加锁的方式
2.1 全局变量(静态分布)的锁
这种锁定义在全局代码段,不需要销毁。
2.2 局部变量(动态分布)的锁
这种锁需要在局部代码段定义和初始化,并且需要手动销毁。
2.3 销毁锁(互斥量)的方式
以上两种锁的使用需要在指定加锁区域进行加锁和解锁。
2.4 互斥量加锁和解锁
2.5 RAII风格的锁
C++ 注重 RAII 编程思想,可以将锁封装成 RAII 风格的锁。
我们可以将锁封装成 LockGuard 类,构造函数加锁,析构函数解锁,这样可以创建局部对象,让编译器自动调用构造和析构函数,无需手动加锁和解锁。
代码:
#ifndef __LOCK_GUARD_HPP__ #define __LOCK_GUARD_HPP__ #include#include class LockGuard { public: LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) { pthread_mutex_lock(_mutex); // 构造加锁 } ~LockGuard() { pthread_mutex_unlock(_mutex); // 析构解锁 } private: pthread_mutex_t *_mutex; }; #endif
在学习了加锁方式后,我们可以优化抢票系统:
代码:
void route(ThreadData *td) {
while (true) {
{
LockGuard guard(&td->_mutex); // 临时对象,RAII风格的加锁和解锁
if (td->_tickets > 0) {
usleep(1000);
printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);
td->_tickets--;
td->_total++;
} else {
break;
}
}
}
}执行结果:

加锁后,票数不再变成负数,问题得到解决。
3. 互斥的底层实现
通过上面的例子,我们意识到简单的 i++ 或 ++i 操作不是原子的,可能导致数据一致性问题。为了实现互斥锁操作,大多数体系结构提供了 swap 或 exchange 指令,这些指令将寄存器和内存单元的数据进行交换,确保原子性,即使在多处理器平台上,访问内存的总线周期也有先后顺序,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待。


所有线程在争锁时,只有一个锁,交换过程只有一条汇编指令,因此是原子的。
CPU 寄存器硬件只有一套,但 CPU 寄存器内部的数据(线程的硬件上下文)有多套。数据在内存中时,所有线程都能访问,属于共享的,但一旦转移到 CPU 内部寄存器,就属于单个线程私有的。










