std::thread构造完成是否真的与执行线程的启动保持同步?
你的核心问题是对C++内存模型中synchronize with术语的理解有偏差,主流标准库的实现完全符合标准要求,不存在违规问题。
1 先搞懂synchronize with的真正含义
C++标准里的同步关系不是指两个事件的执行时间严格一先一后,而是跨线程的内存可见性保证:只要A事件和B事件构成synchronize with关系,那么A发生之前所有的内存写入,对B执行的代码来说都是完全可见的,不需要额外同步。
放到std::thread的场景里,这个规定的实际作用是:你在构造std::thread对象之前、以及构造过程中写入的所有数据(比如传给线程函数的参数、线程函数要访问的全局变量等),新线程执行函数的时候一定能读到最新值,不会出现半写的脏数据。
2 为什么新线程可以在构造函数结束前启动
标准从来没有要求构造函数必须完全执行完才能启动新线程,只要满足可见性要求就符合规范。
现在libc++、libstdc++的实现逻辑都是:
- 第一步:先把线程要执行的函数f、传入的参数全部拷贝一份,写到线程关联的共享存储里,完成所有必要的写入操作
- 第二步:调用操作系统API创建新线程
操作系统调度新线程的时机是不确定的,完全可能刚发起创建线程的系统调用,新线程就被调度执行了,而此时std::thread的构造函数还在做收尾工作(比如给线程对象的内部id字段赋值、处理返回状态等),这就出现了你观察到的「新线程先跑,构造函数还没结束」的情况。
这种实现是完全合法的:因为线程函数f需要用到的所有数据在创建线程前就已经全部写完了,可见性已经得到保障,构造函数剩下的收尾操作不涉及f要访问的内容,哪怕和新线程并行执行也不会违反同步规则。
3 是不是标准和实现有差异?
完全不存在差异,是你之前对同步规则的预期超出了标准的要求。标准只保证可见性,不保证两个事件的执行顺序完全没有重叠,哪怕构造函数和新线程同时运行,只要可见性要求达标,就满足「构造函数完成和f启动构成同步关系」的规定。
内容的提问来源于stack exchange,提问作者Ryoji

