C#如何保证对象完全初始化以避免无同步机制下的线程访问崩溃问题?
你对C的理解完全正确——在C里,编译器或硬件的指令重排确实可能导致线程B拿到对象指针时,对象的虚表或者内部字段还没完成初始化,进而引发崩溃或者未定义行为。但C#的内存模型从设计上就堵死了这个漏洞,咱们来拆解一下它的保障机制:
1. 严格的初始化顺序约束
C#语言规范和CLR内存模型明确规定:当一个对象的引用被写入到任何存储位置(比如你说的全局变量)时,这个对象的所有字段(包括用于实现虚方法的类型元数据,对应C++里的vtable结构)必须已经完成初始化,并且这些初始化操作的结果对后续读取该引用的线程是可见的。
简单来说,编译器和CLR绝对不允许把「把对象引用赋值给全局变量」这个操作,重排到「对象内部初始化完成」之前。线程A创建对象的过程中,一定会先把对象的所有成员(包括类型信息)都初始化好,才会让这个对象的引用对外可见。
2. 引用读写的原子性
在C#中,所有引用类型变量的读写操作都是原子的。这意味着线程B读取那个全局变量时,只有两种可能的结果:要么读到初始的null,要么读到一个完整、有效的对象引用——绝不会出现读到半初始化的引用(比如类似C++里指针只更新了一半的诡异情况)。
3. 虚方法的底层保障
C#里对象的类型元数据(用来确定虚方法调用目标的结构)是对象创建过程中最早完成初始化的部分之一。当对象引用能被其他线程看到时,这部分元数据肯定已经完全就绪,所以线程B调用虚方法时,CLR能准确找到对应的方法实现,不会出现类似C++中vtable未初始化的崩溃场景。
额外提醒
不过要注意:这种保障只针对对象本身的初始化。如果线程A在对象初始化完成后,还在修改对象的其他字段,线程B读取这些字段时可能会看到旧值(因为没有同步机制保障后续修改的可见性),但至少不会因为对象未完全初始化而崩溃。只有当你需要字段修改的跨线程可见性时,才需要用到lock、volatile或者其他同步手段。
备注:内容来源于stack exchange,提问作者Danilo Carvalho

