
目录
[2.4为什么 pthread_cond_wait 需要互斥量?](#2.4为什么 pthread_cond_wait 需要互斥量?)
1.线程互斥
1.1进程线程间的互斥相关背景概念
• 临界资源:被多个线程共享访问的资源称为临界资源
• 临界区:线程中访问临界资源的那部分代码区域
• 互斥:确保同一时间仅有一个线程能进入临界区访问临界资源,从而实现对临界资源的保护
• 原子性:指不可被中断的完整操作,该操作只有完成或未完成两种状态
1.2互斥量mutex
-
通常情况下,线程使用的数据都是局部变量,这些变量的地址空间位于线程栈内,属于单个线程独有,其他线程无法访问这些变量。
-
但在某些情况下,需要在多个线程间共享变量,这类变量被称为共享变量。通过共享数据的机制,可以实现线程间的交互。
-
当多个线程并发操作共享变量时,可能会引发一些问题。
操作共享变量会有问题的售票系统代码:

执行结果:
(注意gcc编译时需链接库文件-lpthread,否则pthread库函数使用不了)

此时我们可以注意到上述票出现了-1和-2的情况!!!!!!!!
要解决以上问题,需要做到三点:
• 互斥性:临界区的执行必须保持独占性,同一时间仅允许一个线程访问。
• 竞争处理:当多个线程同时请求进入空闲临界区时,系统需确保仅有一个线程获得执行权限。
• 非阻塞性:线程在非临界区执行时,不得妨碍其他线程正常进入临界区。
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。

注意:
-- ticket 操作本⾝就不是⼀个原⼦操作

-- 该操作并非原子操作,实际由三条汇编指令组成:
- 加载(load) :将共享变量
ticket从内存读取到寄存器中 - 更新(update):对寄存器中的值执行减1操作
- 存储(store) :将寄存器中的新值写回
ticket的内存地址
(1)互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
• 方法一:静态分配:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
• 方法2:动态分配
cpp
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
• 使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
• 不要销毁⼀个已经加锁的互斥量
• 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
cpp
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
• 当互斥量处于未锁定状态时,该函数会成功锁定互斥量并返回成功状态
• 若调用函数时出现以下情况:
- 其他线程已锁定该互斥量
- 存在多个线程同时申请互斥量但未竞争成功 则pthread_lock调用将进入阻塞状态(执行流被挂起),直到互斥量被解锁
改进上面的售票系统:

执行结果:

发现现在售票系统变成正常!!!!!
1.3互斥量实现原理探究
• 通过上述示例可以看出,单纯的i++或++i操作都不是原子性的,可能引发数据一致性问题。
• 为实现互斥锁操作,多数体系结构提供了swap或exchange指令。该指令用于交换寄存器与内存单元的数据,由于是单条指令,能够保证原子性。即便在多处理器平台上,内存访问存在先后顺序,当某个处理器执行交换指令时,其他处理器的交换指令必须等待总线周期完成。现对lock和unlock的伪代码进行调整如下:

2.线程同步
2.1条件变量
-
当一个线程以互斥方式访问某个变量时,有时会发现由于其他线程尚未改变状态,自己无法进行任何有效操作。
-
例如,某线程访问队列时若发现队列为空,就必须等待,直到其他线程向队列中添加新元素。这种场景正是使用条件变量的典型情况。
注意:
条件变量通常需要配合互斥锁一起使用。
2.2同步概念与竞态条件
• 同步:在保障数据安全的基础上,通过协调线程访问临界资源的顺序来有效预防饥饿问题,这一机制称为同步。
• 竞态条件:指由于时序问题引发的程序异常现象。在多线程环境下,这种因执行顺序不确定性导致的问题尤为常见。
2.3条件变量函数
初始化
cpp
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
参数:cond:要初始化的条件变量
attr:NULL
销毁
cpp
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
参数:cond:要在这个条件变量上等待
mutex:互斥量
唤醒等待
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
简单案例:(来加强我们对条件变量的理解)
• 首先使用PTHREAD_COND/MUTEX_INITIALIZER进行初步测试,暂时忽略其他实现细节

执行结果:

线程按序打印的现象源于其默认在同一个条件变量下等待。每次唤醒操作会优先处理等待队列的首个线程,该线程完成打印任务后会重新进入队列尾部等待。这种机制形成了循环调度的效果。
2.4为什么 pthread_cond_wait 需要互斥量?
• 条件等待是线程间同步的关键机制。单独线程持续等待条件满足是无意义的,必须由其他线程修改共享变量来触发条件变化,并及时通知等待线程。
• 条件的满足必然涉及共享数据变更,因此必须通过互斥锁保护。缺乏互斥锁将无法安全地访问和修改共享数据。

• 按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变 量上不就行了,如下代码:

• 解锁和等待操作必须保持原子性,否则可能出现线程永久阻塞的问题。具体而言,如果在解锁之后、pthread_cond_wait调用之前,其他线程获取了互斥量并发送了信号(此时条件已满足),pthread_cond_wait将无法接收到该信号,从而导致线程持续阻塞。因此,这两个操作必须作为一个不可分割的原子操作执行。
• int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 该函数执行时会首先检查条件变量是否为0:
- 若条件量为0,则执行以下操作:
- 释放互斥锁
- 进入等待状态
- 当函数返回时:
- 重新获取互斥锁
- 将条件量设置为1
2.5条件变量使用规范
•等待条件变量
cpp
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
•给条件发送信号代码
cpp
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);