多线程持久化代码修改后频繁加解锁致性能骤降,求解析
问题场景
程序包含8个写线程和1个持久化线程,初始持久化线程核心代码如下(性能达标:数百万次写入仅需数百毫秒):
std::string longLine; myMutex.lock(); while (!myQueue.empty()) { std::string& head = myQueue.front(); const int hSize = head.size(); if(hSize < blockMaxSize) break; longLine += head; myQueue.pop_front(); } myMutex.unlock(); flushToFile(longLine);
为避免字符串拷贝,修改后的代码性能骤降(数百万次写入需耗时数秒),调试发现大部分时间消耗在flushToFile后等待锁:
myMutex.lock(); while (!myQueue.empty()) { const int hsize = myQueue.front().size(); if(hsize < blockMaxSize) break; std::string head{std::move(myQueue.front())}; myQueue.pop_front(); myMutex.unlock(); flushToFile(head); myMutex.lock(); } myMutex.unlock();
性能暴跌的核心原因
锁竞争被大幅放大
原代码中,持久化线程仅在内存批量处理阶段持有锁:一次性取出所有符合条件的队列元素,拼接字符串的操作都是内存级别的,耗时极短,锁的持有时间非常短。8个写线程大部分时间能顺利获取锁往队列写入数据,锁冲突概率极低。
修改后的代码,每次仅取出一个元素就释放锁,随后执行flushToFile——这是磁盘IO操作,耗时远长于内存操作。在持久化线程执行IO的这段时间里,8个写线程会疯狂竞争并持有锁,持续往队列写入数据。当持久化线程完成IO后尝试重新获取锁时,锁几乎被写线程占满,导致它需要长时间等待锁释放,这就是你看到的“flushToFile后等待锁”的核心原因。锁的频繁切换开销
修改后的代码每次处理一个元素就会经历“加锁→取元素→解锁→IO→加锁”的循环,锁的获取/释放次数呈数量级增长。每次锁切换都存在上下文切换的开销,进一步加剧了性能损耗。IO的串行化放大了延迟
原代码是批量拼接后一次性IO,利用了磁盘IO的批量写入特性(磁盘连续写入的效率远高于零散写入)。修改后变成每次单个元素写入,零散IO的开销本身就远高于批量IO,再加上锁等待的时间,整体性能自然暴跌。
优化建议
如果想避免字符串拷贝同时保留高性能,可以结合批量操作和移动语义:
std::vector<std::string> batch; myMutex.lock(); while (!myQueue.empty()) { const int hSize = myQueue.front().size(); if(hSize < blockMaxSize) break; batch.push_back(std::move(myQueue.front())); myQueue.pop_front(); } myMutex.unlock(); // 可选方案1:批量逐个写入 for(auto&& str : batch) { flushToFile(str); } // 可选方案2:拼接成大字符串后批量写入(业务允许时优先选这个) std::string longLine; for(auto&& str : batch) { longLine += std::move(str); } flushToFile(longLine);
这样既通过std::move避免了字符串拷贝,又保持了批量获取元素的逻辑,锁持有时间依然很短,同时可以选择批量IO来提升写入效率。
内容的提问来源于stack exchange,提问作者Pan Ruochen

