📖 针对Valgrind 并发调试 :同步B站的视频课程讲解
Valgrind 并发调试 · Part 2:DRD 实战——死锁与锁序一网打尽
目标:用最小可复现示例,系统讲清 DRD 如何发现死锁、锁顺序反转与常见锁误用;给出“能直接跑”的命令、输出解读与两种标准修法。

1. DRD 是什么?什么时候选它
DRD (Thread Error Detector) 是 Valgrind 的并发诊断器之一,特点:
- 轻量、速度快:更适合常规巡检或 CI 回归。
- 对锁序问题直观:会统计大量具体冲突实例(冲突密度)。
- 覆盖范围:数据竞争、死锁/锁序反转、条件变量误用、线程 API 误用等。
小结:先 Helgrind 清理 data race,后 DRD 量化锁冲突与复现死锁,两者互补。
2. 必备概念速览
- 互斥量 (mutex):同一时刻仅允许一个线程进入临界区。
- 死锁 (deadlock):两个(或更多)线程彼此等待对方持有的资源而永不前进。
- 锁顺序反转 (lock order inversion):两条路径对多把锁采用相反顺序加锁,极易形成环路等待。
- 形成死锁的四要素:互斥、占有且等待、不剥夺、循环等待(命中这四个就会死锁)。
3. 最小示例:两把锁、相反顺序(偶发死锁)
来自你的代码,加入微小延时扩大竞态窗口。
// deadlock.c
// gcc -O0 -g deadlock.c -pthread -o deadlock
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
static pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
void* thread_A(void* arg) {
pthread_mutex_lock(&m1); // A 先拿 m1
usleep(1000); // 放大竞争窗口
pthread_mutex_lock(&m2); // 再等 m2
printf("A got m1 -> m2\n");
pthread_mutex_unlock(&m2);
pthread_mutex_unlock(&m1);
return NULL;
}
void* thread_B(void* arg) {
pthread_mutex_lock(&m2); // B 先拿 m2(与 A 相反)
usleep(1000);
pthread_mutex_lock(&m1); // 再等 m1
printf("B got m2 -> m1\n");
pthread_mutex_unlock(&m1);
pthread_mutex_unlock(&m2);
return NULL;
}
int main(void) {
pthread_t tA, tB;
pthread_create(&tA, NULL, thread_A, NULL);
pthread_create(&tB, NULL, thread_B, NULL);
pthread_join(tA, NULL);
pthread_join(tB, NULL);
return 0;
}
运行 & 现象
gcc -O0 -g deadlock.c -pthread -o deadlock
./deadlock # 可能卡死,也可能“偶然成功”
- 若发生死锁:程序卡住不退出。
- 若未死锁:输出两行
A got .../B got ...,但这只是非确定性调度带来的侥幸。
用 DRD 检查
valgrind --tool=drd -q ./deadlock
-
可能打印 0 错误(当次未形成环路),也可能给出锁序冲突/互斥量等待的相关提示。关键是:
- 反复运行可观察冲突实例数量(
ERROR SUMMARY: N errors from M contexts)。
- 反复运行可观察冲突实例数量(
4. 让死锁“必现”:同步栅栏对齐(强制构造循环等待)
用
pthread_barrier_t让 A/B 各自拿到第一把锁后同时去抢第二把。
// deadlock_barrier.c
// gcc -O0 -g deadlock_barrier.c -pthread -o deadlock_barrier
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
static pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
static pthread_barrier_t bar;
void* thread_A(void* arg) {
pthread_mutex_lock(&m1);
pthread_barrier_wait(&bar); // 等 B 锁住 m2
pthread_mutex_lock(&m2); // 与 B 形成环路等待 → 死锁
return NULL;
}
void* thread_B(void* arg) {
pthread_mutex_lock(&m2);
pthread_barrier_wait(&bar); // 等 A 锁住 m1
pthread_mutex_lock(&m1); // 与 A 形成环路等待 → 死锁
return NULL;
}
int main(void) {
pthread_barrier_init(&bar, NULL, 2);
pthread_t tA, tB;
pthread_create(&tA, NULL, thread_A, NULL);
pthread_create(&tB, NULL, thread_B, NULL);
pthread_join(tA, NULL);
pthread_join(tB, NULL);
pthread_barrier_destroy(&bar);
return 0;
}
DRD 下的直观表现
valgrind --tool=drd ./deadlock_barrier
- 程序会卡住;用
Ctrl+C结束后,DRD/Helgrind 往往会报告线程退出仍持锁或锁顺序反转。 - DRD 的优势:会把大量冲突实例与访问段统计出来,便于评估冲突密度。
5. 两种标准修法(务必掌握)
5.1 统一加锁顺序(首选)
全项目约定:永远先 m1 再 m2。
// fixed_order.c
// gcc -O0 -g fixed_order.c -pthread -o fixed_order
#include <pthread.h>
static pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
static inline void lock_in_order() {
pthread_mutex_lock(&m1);
pthread_mutex_lock(&m2);
}
static inline void unlock_in_order() {
pthread_mutex_unlock(&m2);
pthread_mutex_unlock(&m1);
}
void* A(void* arg){ lock_in_order(); /* 临界区 */ unlock_in_order(); return NULL; }
void* B(void* arg){ lock_in_order(); /* 临界区 */ unlock_in_order(); return NULL; }
int main(void){ pthread_t a,b; pthread_create(&a,NULL,A,NULL); pthread_create(&b,NULL,B,NULL); pthread_join(a,NULL); pthread_join(b,NULL); }
验证:
valgrind --tool=drd -q ./fixed_order # 应无锁序/死锁告警
5.2 trylock + 退避重试(难统一顺序时)
拿不到第二把锁就放弃第一把,退避片刻后重试,打破“占有且等待”。
// trylock_backoff.c
// gcc -O0 -g trylock_backoff.c -pthread -o trylock_backoff
#include <pthread.h>
#include <unistd.h>
static pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
static void lock_both_with_retry() {
for (;;) {
pthread_mutex_lock(&m1);
if (pthread_mutex_trylock(&m2) == 0) return; // 成功
pthread_mutex_unlock(&m1); // 释放并退避
usleep(1000); // 简单退避策略
}
}
static inline void unlock_both(){ pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); }
void* A(void* arg){ lock_both_with_retry(); /* 临界区 */ unlock_both(); return NULL; }
void* B(void* arg){ lock_both_with_retry(); /* 临界区 */ unlock_both(); return NULL; }
int main(void){ pthread_t a,b; pthread_create(&a,NULL,A,NULL); pthread_create(&b,NULL,B,NULL); pthread_join(a,NULL); pthread_join(b,NULL); }
验证:
valgrind --tool=drd -q ./trylock_backoff # 不应再卡死;如仍有告警,检查退避/释放是否到位
6. DRD 常用参数与读报技巧
-
-q:安静模式,便于聚焦错误摘要。 -
--first-race-only=yes:仅显示首个竞态,减少噪音(调试早期可开)。 -
--exclusive-threshold=<n>:降噪(过滤共享读写的某些模式)。 -
读报三看:
- 地址是否映射到你的共享对象(如
counter、结构体字段)。 - 调用栈是否位于临界区边界(加锁前/后)。
- 上下文数量/错误总数(冲突密度随压力测试陡增)。
- 地址是否映射到你的共享对象(如
7. FAQ(高频坑)
- 为什么我这次 0 错误、下次一堆错误?
并发是非确定性的;请多跑几次、加压(更多循环/线程)、或用 barrier 强制复现。 - 死锁与饥饿的区别?
死锁=各方互等永不前进;饥饿=某线程长期抢不到资源但系统仍在运行。 volatile能避免竞态/死锁吗?
不能。volatile不提供互斥/顺序;请使用互斥量/原子操作。
8. 一页清单(拿去即用)
-
编译:
-O0 -g(行号准确)。 -
先 Helgrind:扫 data race、条件变量误用、线程退出仍持锁。
-
再 DRD:复现实死锁/锁序反转;观察错误数量变化(冲突密度)。
-
修复优先级:
- 统一加锁顺序 > trylock+退避 > 设计简化(减少锁层数,缩短临界区)。
-
CI 体检:把
helgrind+drd都纳入并发回归(压力测试 + 报错即失败)。
9. 结语
- DRD 在死锁与锁序问题上反馈直观、代价较低,适合日常巡检与教学演示;
- 与 Helgrind 搭配,能在真实项目中建立可落地的并发质量保障。
📖 针对Valgrind 并发调试 :同步B站的视频课程讲解
432

被折叠的 条评论
为什么被折叠?



