vptr是否存在数据竞争?C++多线程构造期虚函数调用问题咨询
关于vptr数据竞争与构造期间多线程调用虚函数的问题
核心疑问
- vptr是否会受到数据竞争影响?
- 若IPC在独立线程中调用lambda,这段代码是否存在数据竞争?是不是vtable未完全构造时lambda会调用
Base::Handle(),构造完成则调用Derived::Handle()? - 代码的随机输出是否属于未定义行为?需要等待多久才能让IPC调用派生类的
Handle()?是否需要添加同步机制?
测试代码
#include <thread> #include <chrono> #include <functional> #include <iostream> #include <mutex> #include <condition_variable> struct IPC { IPC() : t([&]{ std::unique_lock lk(mut); cv.wait(lk, [&]{ return (bool)f; }); f(); }) {} void SetHandler(std::function<void()> f_) { { std::unique_lock lk(mut); f = f_; } cv.notify_all(); } std::mutex mut; std::condition_variable cv; std::function<void()> f; std::thread t; }; struct Base { Base() { ipc.SetHandler([this]{ Handle(); }); // Maybe construction of Base takes some time. // Removing this sleep makes the output of the program stabilize on "Derived". std::this_thread::sleep_for(std::chrono::microseconds(10)); } virtual void Handle() { std::cout << "Base" << std::endl; } IPC ipc; }; struct Derived : Base { void Handle() override { std::cout << "Derived" << std::endl; } }; int main() { Derived b; // I only care about interaction with constructors so join here // to delay destruction of b until the test is finished. b.ipc.t.join(); }
运行结果
多次运行后输出随机交替显示“Base”和“Derived”:
developer@WDX591VWD3:~$ g++-10 -std=c++20 hi.cpp -lpthread developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Derived developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Derived developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Derived developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Base developer@WDX591VWD3:~$ ./a.out Derived developer@WDX591VWD3:~$
问题解答
1. 这确实是未定义行为,存在明确的数据竞争
C++对象构造时,vptr的更新是分阶段的:
- 执行
Base构造函数期间,对象的vptr指向Base的vtable; - 当
Base构造完成、进入Derived构造阶段后,vptr才会被更新为指向Derived的vtable。
你的代码中,Base构造函数里通过ipc.SetHandler把捕获了this的lambda传给了另一个线程——此时Derived对象还在构造中,属于未完全构造的对象。IPC线程可能在Derived构造完成前就调用Handle():
- 如果调用发生在
Base构造阶段或Derived构造未完成时,vptr还指向Base的vtable,就会调用Base::Handle(); - 如果调用发生在
Derived构造完成后,vptr已更新,就会调用Derived::Handle()。
而主线程修改vptr(在构造阶段切换vtable)和IPC线程读取vptr(调用虚函数)之间没有任何同步机制,这完全符合C++标准中数据竞争的定义:对同一内存位置的非原子操作,一个写操作和一个读操作没有同步,直接导致未定义行为。
2. 靠sleep等待完全不可靠,必须用显式同步
你无法通过固定时长的sleep来保证对象构造完成——线程调度时机完全由操作系统决定,不同环境、不同运行时刻的调度情况都可能不同,今天sleep10微秒能用,明天可能就不行了。
3. 修复方案
必须确保IPC线程调用Handle()时,Derived对象已经完全构造完毕,有两种常见思路:
- 延迟注册回调:不在
Base构造函数中注册IPC handler,而是给Base添加一个InitIPC()方法,在main中构造完Derived b之后,手动调用b.InitIPC()来注册回调。这样能保证对象完全构造后,其他线程才会访问它。 - 添加构造完成同步:如果必须在构造期间注册,可以在
Derived构造完成后,通过条件变量或原子标志通知IPC线程,只有收到“构造完成”的信号后,才允许调用Handle()。
内容的提问来源于stack exchange,提问作者xdaimon
相关产品推荐
相关产品推荐

