并发与竞态
什么是并发呢?什么是竞态
并发就是同时存在多条能独立推进、且彼此不可预期对方进度的执行路径,这些路径对同一份状态拥有读写机会,从而带来“谁先谁后”的不确定性。这个不确定性可能会导致同一个时间对同一个资源的访问,这就是竞态。并发的价值在于“让事情看起来同时进行”,代价是“必须主动管理这些时间线之间的交叉点”,否则就会出现“字被盖掉”的竞态。并发是能力,竞态是 bug。
为了处理这个bug,我们就有保护这个资源,让他在一个时刻只能被一个人所用,当有人在拥有这个资源的使用权时,其他人不能使用,这就是锁。
我们要建立临界区: 即在任意给定时刻,代码只能被一个线程执行。
- 信号量
用计算机语言说:
信号量 = 一个整型计数器 + 两条原子操作
• P / wait / down:计数器减 1;若结果 < 0,则阻塞(去睡觉)。
• V / signal / up:计数器加 1;若结果 ≤ 0,则唤醒一个等待者。
计数器可以为任意非负初值,表示“可用资源数”。
N = 1 时退化为互斥锁(一次只能一个人)。
N > 1 时称为计数信号量,允许多个并发使用者。
操作信号量必须是原子的,加减判断的过程不能被打断。当资源暂时不可得时,线程可以睡眠(与自旋锁不同)。
在当前6.10+的内核中,信号量是这样实现的:
1 | /* include/linux/semaphore.h */ |
想用信号量,肯定要先初始化的啦。分为静态初始化和动态运行期初始化
1 |
|
初始化值后就是要用,自然要先了解一下用的逻辑:
核心算法:睡眠/唤醒计数器
(1) 获取(down)
- 如果 count > 0,直接减 1 后返回。
- 否则将当前任务封装成 semaphore_waiter 插入 wait_list,设置状态为 TASK_UNINTERRUPTIBLE,然后 schedule() 睡眠。
- 被唤醒后再次检查 count,成功则减 1 退出,否则继续睡眠。
(2) 释放(up)
- 原子地将 count 加 1。
- 如果加 1 后仍 ≤0,说明有等待者,从 wait_list 摘取第一个 waiter,将其唤醒(wake_up_process)。
具体流程:
• 初始 count = 3
down → 2 → 1 → 0,都直接返回;
第 4 次 down 时 count = 0,于是把任务挂起并把 count 设为 -1;
第 5 次 down 时 count = -1,再挂一个任务并把 count 设为 -2;
……
此时 count == -n 就说明有 n 个等待者。
• up() 时把 count++:
如果加完后 count 仍是负数(≤0),说明还有人在睡觉,于是唤醒链表头的一个任务;
被唤醒的任务重新尝试 down(),把 count 再减 1——于是 count 从 -1 变 0,或从 0 变 1,依此类推。
api:
| 接口 | 说明 |
|---|---|
| void down(struct semaphore *sem) | 不可中断睡眠,直到获得 |
| int down_interruptible(struct semaphore *sem) | 可中断版本,返回 -EINTR |
| int down_killable(struct semaphore *sem) | 可被致命信号打断 |
| int down_trylock(struct semaphore *sem) | 不睡眠,立即返回 0/-EAGAIN |
| void up(struct semaphore *sem) | 释放资源,唤醒等待者 |
烦了,信号量的知识看linux内核设计与实现的笔记去。
这里只讲咋用。