C++20线程安全与代码设计选择的技术疑问
《C++20完全指南》示例代码技术疑问解答
示例代码
#include <iostream> #include <array> #include <thread> #include <latch> using namespace std::literals; // for duration literals void loopOver(char c) { // loop over printing the char c: for (int j = 0; j < c/2; ++j) { std::cout.put(c).flush(); std::this_thread::sleep_for(100ms); } } int main() { std::array tags{'.', '?', '8', '+', '-'}; // tags we have to perform a task for // initialize latch to react when all tasks are done: std::latch allDone{tags.size()}; // initialize countdown with number of tasks // start two threads dealing with every second tag: std::jthread t1{[tags, &allDone] { for (unsigned i = 0; i < tags.size(); i += 2) { // even indexes loopOver(tags[i]); // signal that the task is done: allDone.count_down(); // atomically decrement counter of latch } //... }}; std::jthread t2{[tags, &allDone] { for (unsigned i = 1; i < tags.size(); i += 2) { // odd indexes loopOver(tags[i]); // signal that the task is done: allDone.count_down(); // atomically decrement counter of latch } //... }}; //... // wait until all tasks are done: std::cout << "\nwaiting until all tasks are done\n"; allDone.wait(); // wait until counter of latch is zero std::cout << "\nall tasks done\n"; // note: threads might still run //... }
问题解答
1. 按值捕获tags而非引用的优势
- 彻底规避悬空引用风险:
tags是main函数的局部变量,尽管当前代码中std::jthread会在main结束前自动join,但如果后续代码调整为分离线程,引用捕获会导致线程访问已销毁的对象。按值捕获会复制tags的独立副本,副本生命周期与线程闭包绑定,从根源消除悬空引用问题。 - 消除同步与内存可见性开销:线程仅读取
tags,按值捕获后每个线程拥有专属副本,无需考虑跨线程缓存一致性或内存可见性问题,访问效率更高,也不需要额外同步机制。 - 提升代码鲁棒性:即使后续
main中对tags进行修改(当前代码无此操作),线程持有的副本不受影响,保证线程始终看到创建时的tags状态,行为更可预测。
2. std::cout.put().flush()组合的线程安全性
C++20标准明确:
std::cout.put()作为单个字符输出操作是线程安全的——不同线程调用put()时,每个字符的输出是原子的,不会出现字符拆分、乱码等未定义行为。flush()操作本身也是线程安全的单个操作,不会与其他流操作产生数据竞争。- 但
put()和flush()的组合并非原子操作:线程A执行完put()后,线程B可能先于A的flush()执行put(),导致两个字符同时存入缓冲区,随后A的flush()会将两者一起输出。这种情况会导致输出顺序混乱,但不属于未定义行为,仅为顺序不可控。
3. 内置线程安全保障的说明
C++标准对标准IO流的线程安全有明确内置保障:
- 同一标准流对象的单个字符输入/输出操作是原子的,不存在数据竞争导致的未定义行为,因此
std::cout.put()无需额外显式同步。 flush()作为单个流操作,同样具备内置线程安全,不会与其他流操作冲突。- 需注意:这种保障仅针对单个字符操作,多字符输出(如
std::cout << "string")不保证原子性,可能被其他线程输出打断;同时,多个操作的组合(如put()+flush())也不保证原子性,仅能保证每个单独操作的安全性。
内容的提问来源于stack exchange,提问作者sam
相关产品推荐
相关产品推荐

