我怀疑在某些 C++ 多线程情况下可能存在竞争条件,涉及 vtable 动态调度实现中的虚拟方法调用(为此,vtable 指针作为隐藏成员存储在具有虚拟方法的对象中)。我想确认这是否真的是一个问题,并且我指定了 boost 的线程库,以便我们可以假设一些参考框架。
假设一个对象“O”有一个 boost::mutex 成员,它的整个构造函数/析构函数和方法都被范围锁定(类似于 Monitor 并发模式)。线程“A”在没有外部同步的情况下在堆上构造对象“O”(即没有包含“新”操作的共享互斥锁,它可以与其他线程同步;但请注意,仍然存在“内部,Monitor”互斥锁其构造函数的作用域)。然后,线程 A 通过同步机制将指向“O”实例(它刚刚构建)的指针传递给另一个线程“B”——例如,同步的读写器队列(注意:只有指向对象正在被传递——而不是对象本身)。施工后,
线程“B”从同步队列中读取对象“O”的指针值,然后立即离开保护队列的临界区。然后线程“B”对对象“O”执行虚方法调用。这是我认为可能出现问题的地方。
现在,我对动态调度的[很可能的] vtable 实现中的虚拟方法调用的理解是,调用线程“B”必须取消引用指向“O”的指针,以便获得存储为其对象的隐藏成员的 vtable 指针,并且这发生在进入方法体之前(自然是因为在访问存储在对象本身中的 vtable 指针之前,要执行的方法体不能安全准确地确定)。假设上述陈述对于这样的实现可能是正确的,这不是竞争条件吗?
由于在任何内存可见性保证操作发生之前(即获取对象“O”中的成员变量mutex),线程“B”(通过解除指向位于堆中的对象“O”的指针)检索vtable指针,那么不确定“B”会感知“A”最初写在对象“O”的构造上的vtable指针值,对吗?(即,它可能会感知到垃圾值,从而导致未定义的行为,对吗?)。
如果上述是有效的可能性,这是否意味着对线程之间共享的专有内部同步对象进行虚拟方法调用是未定义的行为?
而且——同样——由于标准对 vtable 实现是不可知的,如何保证 vtable 指针在虚拟调用之前对其他线程安全可见?我想人们可以在外部同步(“外部”,例如,“围绕共享互斥锁()/解锁()块”)构造函数调用,然后至少在每个线程中进行初始虚拟方法调用,但这似乎是一些非常不和谐的编程。
所以,如果我的怀疑是真的,那么一个可能更优雅的解决方案是使用内联的非虚拟成员函数来锁定成员互斥体,然后转发到虚拟调用。但是——即使那样——我们能否保证构造函数在 lock() 和 unlock() 的范围内初始化了 vtable 指针来保护构造函数本身?
如果有人可以帮助我澄清这一点并确认/否认我的怀疑,我将不胜感激。
编辑:演示以上内容的代码
class Interface
{
public:
virtual ~Interface() {}
virtual void dynamicCall() = 0;
};
class Monitor : public Interface
{
boost::mutex mutex;
public:
Monitor()
{
boost::unique_lock<boost::mutex> lock(mutex);
// initialize
}
virtual ~Monitor()
{
boost::unique_lock<boost::mutex> lock(mutex);
// destroy
}
virtual void dynamicCall()
{
boost::unique_lock<boost::mutex> lock(mutex);
// do w/e
}
};
// for simplicity, the numbers following each statement specify the order of execution, and these two functions are assumed
// void passMonitorToSharedQueue( Interface * monitor )
// Thread A passes the 'monitor' pointer value to a
// synchronized queue, pushes it on the queue, and then
// notifies Thread B that a new entry exists
// Interface * getMonitorFromSharedQueue()
// Thread B blocks until Thread A notifies Thread B
// that a new 'Interface *' can be retrieved,at which
// point it retrieves and returns it
void threadBFunc()
{
Interface * if = getMonitorFromSharedQueue(); // (1)
if->dynamicCall(); // (4) (ISSUE HERE?)
}
void threadAFunc()
{
Interface * monitor = new Monitor; // (2)
passMonitorToSharedQueue(monitor); // (3)
}
-- 在第 (4) 点,我的印象是“线程 A”写入内存的 vtable 指针值可能对“线程 B”不可见,因为我看不出有任何理由假设编译器会生成代码,以便将 vtable 指针写入构造函数的锁定互斥块中。
例如,考虑多核系统的情况,其中每个内核都有一个专用缓存。根据这篇文章,缓存通常是积极优化的,并且——尽管强制缓存一致性——如果不涉及同步原语,则不会对缓存一致性强制执行严格的排序。
也许我误解了这篇文章的含义,但这并不意味着“A”将 vtable 指针写入构造对象(并且没有迹象表明这种写入发生在构造函数的锁定互斥块中)可能在“B”读取 vtable 指针之前不会被“B”感知?如果 A 和 B 都在不同的内核上执行(“A”在 core0 上,“B”在 core1 上),缓存一致性机制可能会重新排序 core1 缓存中 vtable 指针值的更新(使其一致的更新与 core0 的缓存中的 vtable 指针的值,“A”写)这样它发生在“B”的读取之后......如果我正确解释了这篇文章。