AI错误判断mutex可省略,因其未理解join只阻塞主线程而非重排写入时序,开发者用竞态检测器验证后拒绝合并。
某天下午,一位开发者在某个小 C++ 服务的代码审查中打开了一条一行评论。评论说那个 mutex 没有必要。理由很简短:每个 worker 已经有了本地求和,最终的写入会在所有线程 join 之后发生。模型很冷静,模型错了。
下面的案例是一个简化后的复现,不是生产环境事故。开发者是通过 MonkeyCode 的免费模型访问来运行这次审查的。披露:本文是作为 MonkeyCode 产品推广的一部分撰写的。免费服务器选项让审查循环可以在临时主机上运行,而不需要在开发者的笔记本上跑。
#include <mutex>
#include <thread>
#include <vector>
#include <iostream>
std::mutex sum_mtx;
int shared_sum = 0;
void worker(int start) {
int local = 0;
for (int i = start; i < start + 1000; ++i) {
local += i;
}
std::lock_guard<std::mutex> lk(sum_mtx);
shared_sum += local;
}
int main() {
std::vector<std::thread> pool;
for (int i = 0; i < 8; ++i) {
pool.emplace_back(worker, i * 1000);
}
for (auto& t : pool) {
t.join();
}
std::cout << shared_sum << std::endl;
}
免费模型生成了一份 diff。它移除了锁,直接写入 shared_sum。
int local = 0;
for (int i = start; i < start + 1000; ++i) {
local += i;
}
- std::lock_guard<std::mutex> lk(sum_mtx);
shared_sum += local;
模型声称 join 序列保证了最终的写入是安全的。并非如此。对 shared_sum 的写入仍然发生在每个 worker 线程内部。join 只会阻塞主线程,直到每个 worker 完成。它不会把多次写入重排到一条线程里。
测试工具用 ThreadSanitizer 编译了两种变体,并运行了二十次。ThreadSanitizer 是一个竞态检测器。当两个线程访问同一块内存且没有同步机制、且至少有一次访问是写操作时,它就会报告数据竞态。
#!/usr/bin/env bash
set -euo pipefail
# original.cpp 有 mutex,patched.cpp 没有
for file in original patched; do
c++ -g -O1 -fsanitize=thread -fno-omit-frame-pointer $file.cpp -o $file -pthread
done
for i in $(seq 1 20); do
./original >/dev/null 2>original.$i.log || true
./patched >/dev/null 2>patched.$i.log || true
done
echo original race reports: $(grep -l 'WARNING: ThreadSanitizer' original.*.log | wc -l)
echo patched race reports: $(grep -l 'WARNING: ThreadSanitizer' patched.*.log | wc -l)
第一次运行没有产生警告。第五次产生了一次。竞态不是每次都出现。这才是关键。单独一次执行打过补丁的二进制文件会让人误判。
其中一份报告长这样:
WARNING: ThreadSanitizer: data race
Write of size 4 at address ...
Previous write of size 4 at address ...
原版变体在全部二十次运行中都保持沉默。打过补丁的变体在十四次中产生了警告。
开发者用了一条简单的规则:模型建议涉及锁操作的,只有在竞态检测器保持沉默时才能采纳。
| 变体 | 持有锁 | 20 次运行中竞态报告数 |
|---|---|---|
| original | 是 | 0 |
| patched | 否 | 14 |
这张表不是在说盲目信任检测器。它是在让风险变得可见。
免费模型仍然有帮助。它提出了一个清理方案。开发者不需要去猜审查者可能会说什么。测试工具把建议变成了一个测试。模型不是裁判。它是假设的来源。
ThreadSanitizer 只能捕获实际执行到的竞态。如果测试输入没有触发冲突的代码路径,竞态就会隐藏起来。测试工具也需要支持 sanitizer 的编译器。免费服务器可能有未知的限制。开发者不应该把一次干净的运行当作证明。
无法在构建环境中运行 sanitizer 的团队不应该复制这个模式。只把模型当审查者用的团队不应该用这个来代替人工审查。安全关键代码需要不同的标准。
同样的测试工具可以放在 MonkeyCode 的免费服务器选项后面,如果团队想要一个共享的否决任务的话。否则它在笔记本上跑得很好。