知识回顾:
在前面章节介绍System 信号量时,我们介绍了共享资源、临界资源、互斥等概念,下面我们再来回顾一下
• 共享资源
多线程环境下被多个执行流共同访问的资源称为共享资源
• 临界资源
在多线程执行过程中需要被保护的共享资源称为临界资源
• 临界区
线程中访问临界资源的那段代码区域称为临界区
• 互斥机制
确保在任意时刻最多只有一个执行流能够进入临界区访问临界资源,从而实现对临界资源的保护
• 原子性操作
指不可被中断的操作,该类操作只有两种状态:要么完全执行完毕,要么尚未开始执行
1. 问题引入
在多线程编程中,线程使用的数据主要有两种类型:
局部变量
- 存储在各自的线程栈空间中
- 每个线程拥有独立的变量副本
- 其他线程无法直接访问这些变量
共享变量
- 存储在全局数据区或堆区
- 所有线程都可以访问和修改
- 用于线程间的数据交互和通信
当多个线程并发操作共享变量时,会导致数据竞争(Data Race)问题,表现为:
- 读取脏数据
- 数据不一致
- 程序行为不可预测
示例:
// 操作共享变量会有问题的售票系统代码
#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)
{
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, (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);
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Mutex$ ./test
thread 2 sells ticket:100
thread 3 sells ticket:100
thread 1 sells ticket:100
thread 4 sells ticket:100
thread 2 sells ticket:96
thread 1 sells ticket:95
thread 3 sells ticket:94
thread 4 sells ticket:94
thread 2 sells ticket:92
thread 1 sells ticket:91
thread 4 sells ticket:90
thread 3 sells ticket:90
thread 2 sells ticket:88
thread 1 sells ticket:87
thread 3 sells ticket:86
thread 4 sells ticket:87
thread 2 sells ticket:84
thread 1 sells ticket:83
thread 3 sells ticket:83
thread 4 sells ticket:83
thread 2 sells ticket:80
thread 1 sells ticket:79
thread 3 sells ticket:79
thread 4 sells ticket:77
thread 2 sells ticket:76
thread 1 sells ticket:75
thread 4 sells ticket:74
thread 3 sells ticket:75
thread 2 sells ticket:72
thread 4 sells ticket:71
thread 1 sells ticket:71
thread 3 sells ticket:70
thread 2 sells ticket:68
thread 4 sells ticket:67
thread 3 sells ticket:66
thread 1 sells ticket:65
thread 2 sells ticket:64
thread 4 sells ticket:63
thread 3 sells ticket:62
thread 1 sells ticket:61
thread 2 sells ticket:60
thread 4 sells ticket:59
thread 3 sells ticket:58
thread 1 sells ticket:57
thread 2 sells ticket:56
thread 3 sells ticket:55
thread 4 sells ticket:54
thread 1 sells ticket:53
thread 2 sells ticket:52
thread 3 sells ticket:51
thread 4 sells ticket:50
thread 2 sells ticket:49
thread 3 sells ticket:48
thread 4 sells ticket:47
thread 2 sells ticket:46
thread 3 sells ticket:45
thread 4 sells ticket:44
thread 2 sells ticket:43
thread 3 sells ticket:42
thread 4 sells ticket:41
thread 2 sells ticket:40
thread 3 sells ticket:39
thread 4 sells ticket:38
thread 2 sells ticket:37
thread 3 sells ticket:36
thread 4 sells ticket:36
thread 2 sells ticket:34
thread 3 sells ticket:33
thread 4 sells ticket:32
thread 1 sells ticket:31
thread 2 sells ticket:30
thread 4 sells ticket:29
thread 3 sells ticket:28
thread 1 sells ticket:27
thread 2 sells ticket:26
thread 4 sells ticket:25
thread 3 sells ticket:24
thread 1 sells ticket:23
thread 2 sells ticket:22
thread 4 sells ticket:21
thread 3 sells ticket:20
thread 1 sells ticket:19
thread 2 sells ticket:18
thread 4 sells ticket:17
thread 3 sells ticket:17
thread 1 sells ticket:15
thread 2 sells ticket:14
thread 4 sells ticket:13
thread 3 sells ticket:12
thread 1 sells ticket:11
thread 2 sells ticket:10
thread 4 sells ticket:9
thread 3 sells ticket:8
thread 1 sells ticket:7
thread 2 sells ticket:6
thread 4 sells ticket:5
thread 3 sells ticket:4
thread 1 sells ticket:3
thread 2 sells ticket:2
thread 4 sells ticket:1
thread 3 sells ticket:0
thread 1 sells ticket:-1
thread 2 sells ticket:-2
多个线程同时操作共享变量ticket
导致了不可预测的结果,甚至出现了负数票数。
问题分析:为什么会出现错误结果?
1. 非原子操作
ticket--
操作不是原子的。它对应三条汇编指令:
mov 0x2004e3(%rip),%eax # 将ticket从内存加载到寄存器
sub $0x1,%eax # 寄存器中的值减1
mov %eax,0x2004da(%rip) # 将新值写回内存
2. 执行序列交错
考虑两个线程同时执行ticket--
操作的可能序列:
时间点 | 线程1执行 | 线程2执行 | ticket值 |
---|---|---|---|
t1 | mov (ticket), %eax | 100 | |
t2 | sub $1, %eax | 100 | |
t3 | mov (ticket), %eax | 100 | |
t4 | sub $1, %eax | 100 | |
t5 | mov %eax, (ticket) | 99 | |
t6 | mov %eax, (ticket) | 99 |
最终结果:两个线程各执行了一次减操作,但ticket只减少了1!
为什么会出现负数票数?
问题根源:非原子操作和检查与执行分离
关键问题在于这两行代码:
if (ticket > 0) { // 检查阶段
// ...
ticket--; // 执行阶段
}
这两个操作不是原子的,而且它们之间有一个usleep(1000)
调用,这极大地增加了线程切换的可能性。
详细执行序列分析
让我们通过一个具体的执行序列来看看负数是如何产生的。假设初始ticket = 1
。
时间线分析
时间 | 线程1执行 | 线程2执行 | ticket值 | 说明 |
---|---|---|---|---|
t1 | if (ticket > 0) → true |
1 | 线程1检查票数,发现还有1张票 | |
t2 | 被操作系统中断 | 1 | 线程1的时间片用完,切换到线程2 | |
t3 | if (ticket > 0) → true |
1 | 线程2也检查票数,同样发现还有1张票 | |
t4 | usleep(1000) |
1 | 线程2开始休眠 | |
t5 | 被中断 | 1 | 线程2休眠时被中断,切换回线程1 | |
t6 | usleep(1000) |
1 | 线程1开始休眠 | |
t7 | 被中断 | 1 | 线程1休眠时被中断,切换到线程2 | |
t8 | printf(...) |
1 | 线程2打印售票信息 | |
t9 | ticket-- |
0 | 线程2执行减操作,票数变为0 | |
t10 | 循环结束,退出 | 0 | 线程2完成任务 | |
t11 | 被唤醒 | 0 | 切换回线程1 | |
t12 | printf(...) |
0 | 线程1打印售票信息(但票数已经是0了!) | |
t13 | ticket-- |
-1 | 线程1执行减操作,票数变为-1 |
从汇编层面理解
ticket--
不是原子操作。让我们看看这三条汇编指令如何导致问题:
mov 0x2004e3(%rip),%eax # 将ticket值从内存加载到eax寄存器
sub $0x1,%eax # 将eax寄存器中的值减1
mov %eax,0x2004da(%rip) # 将eax寄存器的新值写回内存
汇编层面的竞态条件
假设ticket = 1
,两个线程交错执行:
时间 | 线程1指令 | 线程2指令 | eax1 | eax2 | ticket |
---|---|---|---|---|---|
t1 | mov (ticket), %eax1 | 1 | - | 1 | |
t2 | sub $1, %eax1 | 0 | - | 1 | |
t3 | mov (ticket), %eax2 | 0 | 1 | 1 | |
t4 | sub $1, %eax2 | 0 | 0 | 1 | |
t5 | mov %eax1, (ticket) | 0 | 0 | 0 | |
t6 | mov %eax2, (ticket) | 0 | 0 | 0 |
在这个序列中,两个线程都执行了减操作,但最终票数只减少了1,而不是2。
更复杂的情况:多个线程交织
当有4个线程同时运行时,情况变得更加复杂。可能的执行序列:
线程1检查
ticket > 0
(为真)线程1被中断,线程2运行
线程2检查
ticket > 0
(为真)线程2被中断,线程3运行
线程3检查
ticket > 0
(为真)线程3执行
ticket--
,票数减为0线程3完成,线程2恢复运行
线程2执行
ticket--
,票数减为-1线程2完成,线程1恢复运行
线程1执行
ticket--
,票数减为-2
为什么 usleep 加剧了问题
usleep(1000)
调用极大地增加了线程切换的概率,因为:
它主动让出CPU,给其他线程运行机会
睡眠时间足够长,操作系统很可能会进行多次线程切换
这放大了检查与执行之间的时间窗口,使得竞态条件更容易发生
2. 互斥量Mutex
要解决共享变量并发访问问题,必须满足以下三个核心条件:
互斥性:当某个线程正在执行临界区代码时,其他线程均不得进入该临界区。
唯一准入:若多个线程同时请求执行临界区代码且当前无线程处于临界区,则仅允许其中一个线程进入。
非阻塞性:未处于临界区的线程不得阻碍其他线程进入临界区。
实现这些条件的关键在于引入锁机制,Linux系统中提供的这种锁称为互斥量(Mutex)。
2.1 互斥量接口详解
互斥量初始化
1. 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
特点:
在编译时初始化互斥量
只能用于全局或静态互斥量
使用默认属性初始化
不需要显式销毁
使用场景:简单的全局互斥量,不需要特殊属性
2. 动态初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex
:指向要初始化的互斥量的指针attr
:互斥量属性,通常为NULL表示使用默认属性
返回值:成功返回0,失败返回错误码
特点:
在运行时初始化互斥量
可以用于堆上或栈上分配的互斥量
可以使用自定义属性
需要显式销毁
使用示例
// 在函数内部动态初始化互斥量
pthread_mutex_t mutex;
if (pthread_mutex_init(&mutex, NULL) != 0) {
// 处理初始化失败
perror("Failed to initialize mutex");
exit(EXIT_FAILURE);
}
互斥量销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:要销毁的互斥量
返回值:成功返回0,失败返回错误码
重要注意事项
不要销毁已加锁的互斥量:这会导致未定义行为
确保没有线程等待锁:销毁前确保没有线程试图获取这个锁
静态初始化的互斥量不需要销毁
销毁后不要再次使用:销毁后的互斥量不能再被使用,除非重新初始化
使用示例
// 使用动态初始化的互斥量后销毁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
// 使用互斥量...
pthread_mutex_destroy(&mutex); // 不再需要时销毁
互斥量加锁和解锁
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
行为:
如果互斥量未被锁定,则锁定它并立即返回
如果互斥量已被锁定,则调用线程会阻塞,直到互斥量可用
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
行为:释放互斥量,允许其他线程获取它
返回值处理
两个函数都返回0表示成功,非0表示错误。应该总是检查返回值:
if (pthread_mutex_lock(&mutex) != 0) {
// 处理错误
perror("Failed to lock mutex");
// 适当的错误处理
}
// 临界区代码
if (pthread_mutex_unlock(&mutex) != 0) {
// 处理错误
perror("Failed to unlock mutex");
// 适当的错误处理
}
其他有用的互斥量函数
非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
行为:
如果互斥量未被锁定,则锁定它并返回0
如果互斥量已被锁定,则立即返回EBUSY,而不是阻塞
使用场景:当不想阻塞当前线程时使用
示例
if (pthread_mutex_trylock(&mutex) == 0) {
// 成功获取锁,执行临界区代码
pthread_mutex_unlock(&mutex);
} else {
// 锁已被占用,执行其他操作
printf("Mutex is busy, doing something else...\n");
}
带超时的加锁
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
行为:尝试获取锁,但在指定的绝对时间前超时
参数:
mutex
:要锁定的互斥量abs_timeout
:绝对超时时间
返回值:成功返回0,超时返回ETIMEDOUT,错误返回其他值
示例
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 2; // 2秒后超时
if (pthread_mutex_timedlock(&mutex, &ts) == 0) {
// 成功获取锁
pthread_mutex_unlock(&mutex);
} else {
// 超时或错误
printf("Failed to acquire lock within 2 seconds\n");
}
互斥量属性
虽然通常使用NULL(默认属性),但有时可能需要自定义属性:
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
// 初始化属性对象
pthread_mutexattr_init(&attr);
// 设置属性(例如设置为递归互斥量)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 使用属性初始化互斥量
pthread_mutex_init(&mutex, &attr);
// 使用完后销毁属性对象
pthread_mutexattr_destroy(&attr);
常见的互斥量类型
PTHREAD_MUTEX_NORMAL:标准互斥量,不检测死锁
PTHREAD_MUTEX_ERRORCHECK:错误检查互斥量,检测死锁
PTHREAD_MUTEX_RECURSIVE:递归互斥量,允许同一线程多次加锁
PTHREAD_MUTEX_DEFAULT:默认类型,通常等同于PTHREAD_MUTEX_NORMAL
调用 pthread_mutex_lock 时,可能会遇到以下几种具体情况:
- 成功获取锁的情况:
- 当互斥量处于未锁定状态(mutex 的值为 0)
- 该函数会原子性地将互斥量状态改为锁定状态(通常设置为调用线程的 ID)
- 函数立即返回 0 表示加锁成功
- 示例场景:首次加锁时,或者前一个持有锁的线程已经调用 pthread_mutex_unlock 释放锁
- 阻塞等待的情况:
- 当检测到互斥量已被其他线程锁定时(mutex 值不为 0)
- 或者有多个线程同时调用 pthread_mutex_lock 尝试获取同一个互斥量
- 系统会根据线程调度策略选择一个线程获得锁,其他线程则被阻塞
- 被阻塞线程的执行流会被挂起,放入该互斥量的等待队列中
- 线程会一直等待,直到锁持有者调用 pthread_mutex_unlock 释放锁
- 系统会从等待队列中唤醒一个线程(具体唤醒策略取决于实现)
- 被唤醒线程会重新尝试获取锁
- 特殊情况说明:
- 如果是递归互斥锁(PTHREAD_MUTEX_RECURSIVE),同一个线程重复加锁不会阻塞
- 如果使用非阻塞模式(PTHREAD_MUTEX_NORMAL),获取不到锁会立即返回 EBUSY
- 每个阻塞的线程都会按照一定的优先级策略排队等待
- 在 Linux 中,默认使用公平的 FIFO 唤醒策略
- 性能考虑:
- 频繁的锁竞争会导致大量的线程上下文切换
- 在设计时应尽量减少临界区的范围和执行时间
- 对于高并发场景,可以考虑使用读写锁或更高级的同步机制
2.2 改进上面的售票系统
静态初始化:
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 静态初始化
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&lock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
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);
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Mutex$ ./test
thread 1 sells ticket:100
thread 1 sells ticket:99
thread 1 sells ticket:98
thread 1 sells ticket:97
thread 1 sells ticket:96
thread 1 sells ticket:95
thread 1 sells ticket:94
thread 1 sells ticket:93
thread 1 sells ticket:92
thread 1 sells ticket:91
thread 1 sells ticket:90
thread 1 sells ticket:89
thread 1 sells ticket:88
thread 1 sells ticket:87
thread 1 sells ticket:86
thread 1 sells ticket:85
thread 1 sells ticket:84
thread 1 sells ticket:83
thread 1 sells ticket:82
thread 1 sells ticket:81
thread 1 sells ticket:80
thread 1 sells ticket:79
thread 1 sells ticket:78
thread 1 sells ticket:77
thread 1 sells ticket:76
thread 1 sells ticket:75
thread 1 sells ticket:74
thread 1 sells ticket:73
thread 1 sells ticket:72
thread 1 sells ticket:71
thread 1 sells ticket:70
thread 1 sells ticket:69
thread 1 sells ticket:68
thread 1 sells ticket:67
thread 1 sells ticket:66
thread 1 sells ticket:65
thread 1 sells ticket:64
thread 1 sells ticket:63
thread 1 sells ticket:62
thread 1 sells ticket:61
thread 1 sells ticket:60
thread 1 sells ticket:59
thread 1 sells ticket:58
thread 1 sells ticket:57
thread 1 sells ticket:56
thread 1 sells ticket:55
thread 1 sells ticket:54
thread 1 sells ticket:53
thread 1 sells ticket:52
thread 1 sells ticket:51
thread 1 sells ticket:50
thread 1 sells ticket:49
thread 1 sells ticket:48
thread 1 sells ticket:47
thread 1 sells ticket:46
thread 1 sells ticket:45
thread 1 sells ticket:44
thread 1 sells ticket:43
thread 1 sells ticket:42
thread 1 sells ticket:41
thread 1 sells ticket:40
thread 1 sells ticket:39
thread 1 sells ticket:38
thread 1 sells ticket:37
thread 1 sells ticket:36
thread 1 sells ticket:35
thread 1 sells ticket:34
thread 1 sells ticket:33
thread 1 sells ticket:32
thread 1 sells ticket:31
thread 1 sells ticket:30
thread 1 sells ticket:29
thread 1 sells ticket:28
thread 1 sells ticket:27
thread 1 sells ticket:26
thread 1 sells ticket:25
thread 1 sells ticket:24
thread 1 sells ticket:23
thread 1 sells ticket:22
thread 1 sells ticket:21
thread 1 sells ticket:20
thread 1 sells ticket:19
thread 1 sells ticket:18
thread 1 sells ticket:17
thread 1 sells ticket:16
thread 1 sells ticket:15
thread 1 sells ticket:14
thread 1 sells ticket:13
thread 1 sells ticket:12
thread 1 sells ticket:11
thread 1 sells ticket:10
thread 1 sells ticket:9
thread 1 sells ticket:8
thread 1 sells ticket:7
thread 1 sells ticket:6
thread 1 sells ticket:5
thread 1 sells ticket:4
thread 1 sells ticket:3
thread 1 sells ticket:2
thread 1 sells ticket:1
可以看到我们加了锁之后,只有线程1在访问临界资源,其余线程不能访问。
动态初始化:
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t lock;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_mutex_lock(&lock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main(void)
{
pthread_mutex_init(&lock, nullptr); // 动态初始化
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);
pthread_mutex_destroy(&lock); // 动态初始化需要显式销毁
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Mutex$ ./test
thread 1 sells ticket:100
thread 1 sells ticket:99
thread 1 sells ticket:98
thread 1 sells ticket:97
thread 1 sells ticket:96
thread 1 sells ticket:95
thread 1 sells ticket:94
thread 1 sells ticket:93
thread 1 sells ticket:92
thread 1 sells ticket:91
thread 1 sells ticket:90
thread 1 sells ticket:89
thread 1 sells ticket:88
thread 1 sells ticket:87
thread 1 sells ticket:86
thread 1 sells ticket:85
thread 1 sells ticket:84
thread 1 sells ticket:83
thread 1 sells ticket:82
thread 1 sells ticket:81
thread 1 sells ticket:80
thread 1 sells ticket:79
thread 1 sells ticket:78
thread 1 sells ticket:77
thread 1 sells ticket:76
thread 1 sells ticket:75
thread 1 sells ticket:74
thread 1 sells ticket:73
thread 1 sells ticket:72
thread 1 sells ticket:71
thread 1 sells ticket:70
thread 1 sells ticket:69
thread 1 sells ticket:68
thread 1 sells ticket:67
thread 1 sells ticket:66
thread 1 sells ticket:65
thread 1 sells ticket:64
thread 1 sells ticket:63
thread 1 sells ticket:62
thread 1 sells ticket:61
thread 1 sells ticket:60
thread 1 sells ticket:59
thread 1 sells ticket:58
thread 1 sells ticket:57
thread 1 sells ticket:56
thread 1 sells ticket:55
thread 1 sells ticket:54
thread 1 sells ticket:53
thread 1 sells ticket:52
thread 1 sells ticket:51
thread 1 sells ticket:50
thread 1 sells ticket:49
thread 1 sells ticket:48
thread 1 sells ticket:47
thread 1 sells ticket:46
thread 1 sells ticket:45
thread 1 sells ticket:44
thread 1 sells ticket:43
thread 1 sells ticket:42
thread 1 sells ticket:41
thread 1 sells ticket:40
thread 1 sells ticket:39
thread 1 sells ticket:38
thread 1 sells ticket:37
thread 1 sells ticket:36
thread 1 sells ticket:35
thread 1 sells ticket:34
thread 1 sells ticket:33
thread 1 sells ticket:32
thread 1 sells ticket:31
thread 1 sells ticket:30
thread 1 sells ticket:29
thread 1 sells ticket:28
thread 1 sells ticket:27
thread 1 sells ticket:26
thread 1 sells ticket:25
thread 1 sells ticket:24
thread 1 sells ticket:23
thread 1 sells ticket:22
thread 1 sells ticket:21
thread 1 sells ticket:20
thread 1 sells ticket:19
thread 1 sells ticket:18
thread 1 sells ticket:17
thread 1 sells ticket:16
thread 1 sells ticket:15
thread 1 sells ticket:14
thread 1 sells ticket:13
thread 1 sells ticket:12
thread 1 sells ticket:11
thread 1 sells ticket:10
thread 1 sells ticket:9
thread 1 sells ticket:8
thread 1 sells ticket:7
thread 1 sells ticket:6
thread 1 sells ticket:5
thread 1 sells ticket:4
thread 1 sells ticket:3
thread 1 sells ticket:2
thread 1 sells ticket:1
2.3 互斥量实现原理探究
经过上面多个线程并发执行i++或++i操作的例子,大家已经清楚地意识到这些看似简单的自增操作实际上并非原子操作。在并发环境下,由于这些操作包含读取、修改和写入三个步骤,中间可能被其他线程打断,因此会导致数据一致性问题。
为了实现互斥锁操作,确保临界区代码的原子性执行,大多数现代计算机体系结构都提供了特殊的硬件指令。其中最常见的是swap或exchange指令(在x86架构中称为XCHG指令),这条指令的作用是将寄存器中的数据和内存单元的数据进行原子性交换。由于这个操作在硬件层面被设计为单条不可分割的指令,因此可以保证其原子性
原子指令:swap/exchange
现代处理器架构提供了一条特殊的原子指令,通常称为"交换"或"比较并交换"指令:
swap/exchange 指令的工作原理
; 假设我们有一个交换指令
xchg [memory], register
这条指令原子性地完成以下操作:
将内存位置的值读取到临时存储
将寄存器的值写入内存位置
将原来的内存值放入寄存器
由于这是一个单一指令,它在执行过程中不会被中断,保证了原子性。
基于交换指令实现互斥锁
下面是使用交换指令实现简单自旋锁的伪代码:
; 假设锁变量在内存地址 LOCK
; 初始值为0表示未锁定,1表示已锁定
acquire_lock:
mov eax, 1 ; 将1放入寄存器
xchg eax, [LOCK] ; 原子交换:将eax与LOCK的值交换
test eax, eax ; 测试eax的值
jnz acquire_lock ; 如果eax不为0(锁已被占用),继续尝试
ret ; 成功获取锁
release_lock:
mov eax, 0 ; 将0放入寄存器
mov [LOCK], eax ; 释放锁(设置为0)
ret
总线锁定与缓存一致性
"访问内存的总线周期也有先后"触及了多处理器系统中的关键问题。
总线锁定机制
当处理器执行原子指令时:
处理器会在执行原子操作期间锁定内存总线
这确保了在此期间没有其他处理器能够访问相同的内存位置
其他处理器的交换指令必须等待当前操作完成
缓存一致性协议
现代多核处理器使用复杂的缓存一致性协议(如MESI协议)来确保所有处理器核心看到相同的内存视图:
Modified(修改):缓存行已被修改,与主内存不同
Exclusive(独占):缓存行只存在于当前缓存中,与主内存相同
Shared(共享):缓存行可能存在于多个缓存中,与主内存相同
Invalid(无效):缓存行不包含有效数据
当执行原子操作时:
如果缓存行处于"共享"状态,处理器会将其升级为"独占"状态
执行原子操作
根据需要将缓存行标记为"修改"状态
3. 互斥量的封装
下面我们来模拟封装一个简易版本的互斥量
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
int n = pthread_mutex_init(&_mutex, nullptr);
if(n != 0)
{
perror("init failed");
}
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
if(n != 0)
{
perror("lock failed");
}
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
if(n != 0)
{
perror("unlock failed");
}
}
~Mutex()
{
int n = pthread_mutex_destroy(&_mutex);
if(n != 0)
{
perror("destroy failed");
}
}
private:
pthread_mutex_t _mutex;
};
}
我们还可以使用RAII的方式,也就是和智能指针一样,来帮我们管理锁。
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
int n = pthread_mutex_init(&_mutex, nullptr);
if (n != 0)
{
perror("init failed");
}
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
if (n != 0)
{
perror("lock failed");
}
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
if (n != 0)
{
perror("unlock failed");
}
}
~Mutex()
{
int n = pthread_mutex_destroy(&_mutex);
if (n != 0)
{
perror("destroy failed");
}
}
private:
pthread_mutex_t _mutex;
};
// 采用RAII风格,进行管理
class LockGuard
{
public:
LockGuard(Mutex& mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex& _mutex;
};
}
下面我们来测试一下:
#include "Mutex.hpp"
#include <unistd.h>
using namespace MutexModule;
int ticket = 100;
Mutex mutex;
void *route(void *arg)
{
char *id = (char *)arg;
while (1)
{
LockGuard lock(mutex);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else
{
break;
}
}
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);
return 0;
}
运行结果:
ltx@iv-ye1i2elts0wh2yp1ahah:~/gitLinux/Linux_system/lesson_thread/Mutex/Lock$ ./test
thread 1 sells ticket:100
thread 1 sells ticket:99
thread 1 sells ticket:98
thread 1 sells ticket:97
thread 1 sells ticket:96
thread 1 sells ticket:95
thread 1 sells ticket:94
thread 1 sells ticket:93
thread 1 sells ticket:92
thread 1 sells ticket:91
thread 1 sells ticket:90
thread 1 sells ticket:89
thread 1 sells ticket:88
thread 1 sells ticket:87
thread 1 sells ticket:86
thread 1 sells ticket:85
thread 1 sells ticket:84
thread 1 sells ticket:83
thread 1 sells ticket:82
thread 1 sells ticket:81
thread 1 sells ticket:80
thread 1 sells ticket:79
thread 1 sells ticket:78
thread 1 sells ticket:77
thread 1 sells ticket:76
thread 1 sells ticket:75
thread 1 sells ticket:74
thread 1 sells ticket:73
thread 1 sells ticket:72
thread 1 sells ticket:71
thread 1 sells ticket:70
thread 1 sells ticket:69
thread 1 sells ticket:68
thread 1 sells ticket:67
thread 1 sells ticket:66
thread 1 sells ticket:65
thread 1 sells ticket:64
thread 1 sells ticket:63
thread 1 sells ticket:62
thread 1 sells ticket:61
thread 1 sells ticket:60
thread 1 sells ticket:59
thread 1 sells ticket:58
thread 1 sells ticket:57
thread 1 sells ticket:56
thread 1 sells ticket:55
thread 1 sells ticket:54
thread 1 sells ticket:53
thread 1 sells ticket:52
thread 1 sells ticket:51
thread 1 sells ticket:50
thread 1 sells ticket:49
thread 1 sells ticket:48
thread 1 sells ticket:47
thread 1 sells ticket:46
thread 1 sells ticket:45
thread 1 sells ticket:44
thread 1 sells ticket:43
thread 1 sells ticket:42
thread 1 sells ticket:41
thread 1 sells ticket:40
thread 1 sells ticket:39
thread 1 sells ticket:38
thread 1 sells ticket:37
thread 1 sells ticket:36
thread 1 sells ticket:35
thread 1 sells ticket:34
thread 1 sells ticket:33
thread 1 sells ticket:32
thread 1 sells ticket:31
thread 1 sells ticket:30
thread 1 sells ticket:29
thread 1 sells ticket:28
thread 1 sells ticket:27
thread 1 sells ticket:26
thread 1 sells ticket:25
thread 1 sells ticket:24
thread 1 sells ticket:23
thread 1 sells ticket:22
thread 1 sells ticket:21
thread 1 sells ticket:20
thread 1 sells ticket:19
thread 1 sells ticket:18
thread 1 sells ticket:17
thread 1 sells ticket:16
thread 1 sells ticket:15
thread 1 sells ticket:14
thread 1 sells ticket:13
thread 1 sells ticket:12
thread 1 sells ticket:11
thread 1 sells ticket:10
thread 1 sells ticket:9
thread 1 sells ticket:8
thread 1 sells ticket:7
thread 1 sells ticket:6
thread 1 sells ticket:5
thread 1 sells ticket:4
thread 1 sells ticket:3
thread 1 sells ticket:2
thread 1 sells ticket:1
📌 RAII风格的互斥锁, C++11也有,比如:
std::mutex mtx;
std::lock_guard<std::mutex> guard(mtx);
这种RAII(Resource Acquisition Is Initialization)风格的锁管理方式具有以下特点:
- 自动加锁解锁:在构造时自动调用mtx.lock(),析构时自动调用mtx.unlock()
- 异常安全:即使临界区代码抛出异常,也能保证锁被释放
- 作用域绑定:锁的生命周期与guard对象的作用域一致