concurrency-debugging

Installation
SKILL.md

并发调试

用途

引导诊断和修复并发 bug:解读 ThreadSanitizer(TSan)竞争报告、使用 Helgrind 进行锁顺序分析、使用 GDB 线程检查检测死锁、识别常见的 std::atomic 误用模式,以及在 C++ 和 Rust 中应用 happens-before 推理。

触发场景

  • "ThreadSanitizer 报告了数据竞争——如何解读报告?"
  • "程序死锁了——如何调试?"
  • "如何使用 Helgrind 查找线程 bug?"
  • "我是否正确使用了 std::atomic?"
  • "C++ 内存顺序中的 happens-before 是如何工作的?"
  • "如何在 GDB 中找出哪些线程发生了死锁?"

工作流程

1. ThreadSanitizer(TSan)——竞争检测

# 使用 TSan 构建
clang -fsanitize=thread -g -O1 -o prog main.c
# 或使用 GCC
gcc -fsanitize=thread -g -O1 -o prog main.c

# 运行(TSan 在运行时拦截内存访问)
./prog

# TSan 特定选项
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" ./prog

解读 TSan 报告:

WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x7f1234 by thread T2:
    #0 increment /src/counter.c:8:5              ← T2 中的访问点
    #1 worker_thread /src/counter.c:22:3

  Previous read of size 4 at 0x7f1234 by thread T1:
    #0 read_counter /src/counter.c:3:14          ← T1 中的冲突访问
    #1 main /src/counter.c:30:5

  Thread T2 created at:
    #0 pthread_create .../tsan_interceptors.cpp
    #1 main /src/counter.c:28:3

SUMMARY: ThreadSanitizer: data race /src/counter.c:8:5 in increment

解读方法:

  1. 第 1 行:访问类型(写/读)和地址
  2. "Write of size" 下的调用栈:执行写操作的线程
  3. "Previous read/write" 下的调用栈:冲突的线程
  4. "Thread T2 created at":线程在哪里被创建
  5. 修复:incrementread_counter 函数在没有同步的情况下访问同一地址

常见竞争及修复方法:

竞争模式 修复
无锁读写全局变量 添加互斥锁或使用 std::atomic
atomic 的双重检查锁定 使用 std::once_flag + std::call_once
对共享整数执行 += 使用 std::atomic<int>::fetch_add()
迭代时修改容器 锁定整个临界区
shared_ptr 引用计数竞争 引用计数已安全(是原子的);但指向的对象可能不安全

2. Helgrind——锁顺序与竞争检测

Helgrind 使用 Valgrind 基础设施检测锁顺序违规(潜在死锁)和数据竞争:

# 使用 Helgrind 运行
valgrind --tool=helgrind --log-file=helgrind.log ./prog

# 锁顺序违规报告
==1234== Thread #3: lock order "0x... M2" after "0x... M1"
==1234== observed (incorrect) order
==1234==    at pthread_mutex_lock (helgrind/...)
==1234==    by worker2 /src/worker.c:45           ← T3 先获取 M2 后获取 M1
==1234==
==1234== required order established by acquisition of lock at address 0x... M1
==1234==    at pthread_mutex_lock
==1234==    by worker1 /src/worker.c:31            ← T1 先获取 M1 后获取 M2

锁顺序违规 = 潜在死锁:

  • 线程 T1 获取 M1,然后尝试获取 M2
  • 线程 T2 获取 M2,然后尝试获取 M1
  • 两者竞争时都可能死锁

修复:强制执行一致的全局锁顺序。始终先获取 M1 再获取 M2。

3. 使用 GDB 检测死锁

# 将 GDB 附加到死锁进程
gdb -p $(pgrep prog)

# 或在 GDB 下运行后触发死锁

(gdb) info threads          # 列出所有线程及当前状态
# * 1  Thread 0x... (LWP 1234) "prog" ... in __lll_lock_wait ()
#   2  Thread 0x... (LWP 1235) "prog" ... in __lll_lock_wait ()
# 阻塞在 __lll_lock_wait 的线程 = 等待互斥锁

(gdb) thread 1
(gdb) bt                    # 显示线程 1 在等待哪个互斥锁

(gdb) thread 2
(gdb) bt                    # 显示线程 2 持有/等待哪个互斥锁

# 查找互斥锁的持有者
(gdb) p ((pthread_mutex_t*)0x601090)->__data.__owner   # Linux glibc 互斥锁
# 打印持有线程的 TID

# Python 脚本转储所有互斥锁持有者(GDB 7+)
python
import gdb
for t in gdb.selected_inferior().threads():
    t.switch()
    print(f"Thread {t.num}: {gdb.execute('bt 3', to_string=True)}")
end

4. std::atomic 误用模式

// 错误:原子变量,但复合操作不是原子的
std::atomic<int> counter{0};
if (counter == 0) counter = 1;   // 不是原子的!TOCTOU 竞争

// 正确:使用 compare_exchange
int expected = 0;
counter.compare_exchange_strong(expected, 1);

// 错误:对同步标志使用 relaxed 顺序
std::atomic<bool> ready{false};
// 生产者:
data = 42;
ready.store(true, std::memory_order_relaxed);  // 错误:无 happens-before

// 正确:使用 release-acquire 发布数据
// 生产者:
data = 42;
ready.store(true, std::memory_order_release);   // 与 acquire 同步

// 消费者:
if (ready.load(std::memory_order_acquire)) {    // 与 release 同步
    use(data);  // 此处可以安全读取 data
}

// 错误:跨线程使用数据而不加原子/互斥锁
// int shared_data;  // 非原子 — 并发访问是未定义行为

// 正确:用互斥锁保护或使用原子类型
std::mutex mtx;
std::unique_lock lock(mtx);
shared_data = 42;

5. happens-before 推理

在 C++ 中,happens-before 通过以下方式建立:

顺序先于(单线程内):
  代码中语句 A 在 B 之前 → A happens-before B

同步于(跨线程):
  在同一原子变量上 store(release) → load(acquire)
    → store happens-before load
    → store 之前的一切 happens-before load 之后的一切

线程创建/等待:
  spawn(T) → T 中的任何操作         (创建与开始同步)
  T 中的任何操作 → join(T)          (join 与结束同步)

互斥锁:
  unlock(M) → lock(M)(下一个获取者)
// 跨线程建立 happens-before
std::atomic<int> flag{0};
int data = 0;

// 线程 1:
data = 42;                        // A
flag.store(1, memory_order_release); // B:A 顺序先于 B

// 线程 2:
while (flag.load(memory_order_acquire) != 1) {}  // C:与 B 同步
int x = data;                     // D:C 顺序先于 D
// D 读取 42:A happens-before B 同步于 C 顺序先于 D
//             → A happens-before D

6. Rust 并发——编译期保证

Rust 通过所有权在编译期阻止数据竞争:

use std::sync::{Arc, Mutex};
use std::thread;

// 共享可变状态:Arc<Mutex<T>>
let counter = Arc::new(Mutex::new(0u32));

let c = Arc::clone(&counter);
let t = thread::spawn(move || {
    let mut val = c.lock().unwrap();
    *val += 1;
});

t.join().unwrap();
println!("{}", *counter.lock().unwrap());

// Rust 阻止:
// - 跨线程共享 &mut T(Sync 未为 &mut T 实现)
// - 将非 Send 类型移入线程(编译器错误)
// 如需 TSan 检查,可与 cargo test 配合使用:
// RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test

相关技能

  • 使用 skills/runtimes/sanitizers 了解 TSan 构建标志和其他 Sanitizer
  • 使用 skills/profilers/valgrind 了解 Helgrind 和 Memcheck 集成
  • 使用 skills/debuggers/gdb 进行高级 GDB 线程检查
  • 使用 skills/low-level-programming/memory-model 了解 C++/Rust 内存顺序理论
Related skills

More from killvxk/low-level-dev-skills-zh

Installs
1
First Seen
Mar 21, 2026