并发理论与同步机制的可视化实验室
八个交互式模块,将抽象的并发原语——锁、信号量、管程、CAS——转化为可观察、可操控的动态系统。每个模块包含 SVG 可视化、实时控制器与统计指标,帮助你建立对竞争、死锁与无锁编程的直觉。
互斥与锁
多个线程竞争同一把互斥锁进入临界区。同一时刻仅允许一个线程持有锁,其余线程进入等待队列,直至锁被释放后按序唤醒。
互斥锁(Mutex)保证临界区的互斥访问。线程执行 lock() 请求所有权:若锁空闲则获取并进入;若已被占用则阻塞于等待队列。unlock() 释放锁并唤醒一个等待者。
- 思考(空闲)→ 请求 → 等待/持有
- 持有者执行临界区后释放,唤醒队首
- 竞争次数反映锁的争用程度
生产者消费者
有界缓冲区上的经典同步问题。生产者在缓冲区满时阻塞,消费者在空时阻塞,通过信号量 empty/full 协调两者速率。
使用三个信号量:empty(空槽位)、full(已填充)、mutex(缓冲区互斥)。生产者 P(empty)→P(mutex)→放入→V(mutex)→V(full);消费者对称。
- 满时生产者阻塞于 empty,空时消费者阻塞于 full
- mutex 保护缓冲区数据一致性
- 速率失配导致一方持续阻塞
哲学家就餐
五位哲学家围坐圆桌,需同时拿起左右两把叉子才能进餐。朴素策略会引发死锁,资源分级与超时回退是两种经典解法。
朴素策略:每人先拿左叉再拿右叉,若同时拿左叉则循环等待——死锁。资源分级:始终先取编号较小的叉子,打破循环等待;超时回退:拿不到右叉则释放左叉并重试。
- 死锁四条件:互斥、持有等待、不剥夺、循环等待
- 分级破坏循环等待,超时破坏持有等待
读者写者
共享数据的读写锁:多读者可并发读,写者必须独占。读者优先可能饿死写者,写者优先则抑制新读者。
读写锁维护一个读计数与写标志。读者优先:读者无需等待其他读者,仅当有写者时阻塞——可能饿死写者。写者优先:只要有写者在等待,新读者必须阻塞——保证写者最终执行。
- 读-读兼容,读-写/写-写互斥
- 优先级策略决定公平性与吞吐权衡
死锁检测
资源分配图中,进程请求资源、资源分配给进程。通过等待图(wait-for graph)检测环——环中的进程即处于死锁状态。
单实例资源系统中,将"进程 P 请求被进程 Q 占有的资源"化简为等待边 P→Q。若等待图存在环,则环上进程死锁。算法通过 DFS 染色(白/灰/黑)发现回边即定位环。
- 请求边:进程 → 资源(虚线)
- 分配边:资源 → 进程(实线)
- 等待图环 ⟺ 死锁
信号量
计数信号量用整数控制资源许可数:P 操作减一,为零则阻塞;V 操作加一或唤醒一个等待者。二元信号量退化为互斥锁。
信号量 S 是带等待队列的整数。P(S):若 S>0 则 S-- 否则阻塞入队;V(S):若有等待者则唤醒一个,否则 S++。计数信号量可管理多实例资源,二元信号量取值 0/1 等价于互斥锁。
- P/V 是原子原语(通常由硬件或内核保证)
- 可用于同步(事件计数)与互斥
管程
管程把共享数据与对其的操作封装在一起,内置互斥。条件变量配合 wait/signal 实现同步:满则生产者 wait,空则消费者 wait。
管程内同一时刻仅一个线程活跃。条件变量 cv 提供 wait(cv)(释放锁、阻塞)与 signal(cv)(唤醒一个等待者)。生产者-消费者用 notFull、notEmpty 两个条件变量协调。
- wait 自动释放管程锁并进入 cv 队列
- signal 唤醒后唤醒者/被唤醒者之一继续
CAS 与无锁
Compare-And-Swap 是无锁编程的硬件原语:比较内存值与预期,相等则写入新值。CAS 循环实现无锁计数器,但存在 ABA 问题。
CAS(addr, expected, new) 原子地:若 *addr==expected 则 *addr=new 并返回成功,否则返回失败。ABA:值 A→B→A 后,CAS 误以为未变化而成功。引入版本号(tag)使每次写入版本递增即可检测。
- 无锁计数器:循环重试直至 CAS 成功
- ABA 危害:栈/链表的悬空指针复用
- 版本号 / hazard pointer / RCU 解决 ABA