0

我怀疑在某些 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”的读取之后......如果我正确解释了这篇文章。

4

4 回答 4

0

在具有隐式缓存的共享内存多处理器系统中,您需要一个内存屏障来使对主内存的更改对其他缓存可见。通常,您可以假设获取或释放任何操作系统同步原语(以及任何构建在它们之上的)具有完整的内存屏障,以便在获取(或释放)同步原语之前发生的所有写入在您获取之后对所有处理器都是可见的它(或释放)。

对于您的具体问题,您内部有一个内存屏障Monitor::Monitor(),因此当它返回时,vtable 将至少被初始化为Monitor::vtable. 如果您从 派生可能会出现问题Monitor,但在您发布的代码中您没有,所以这不是问题。

如果您真的想确保在调用时获得正确的 vtable,则在调用getMonitorFromSharedQueue()之前应该有一个读取障碍if->dynamicCall()

于 2010-07-06T19:00:28.000 回答
0

如果我试图理解你的文章,我相信你在问这个:-

线程“A”在堆上构造对象“O”,无需外部同步

// global namespace
SomeClass* pClass = new SomeClass;

同时,您说线程-'A' 将上述实例传递给线程-'B'。这意味着该实例SomeClass已完全构造或者您是否尝试将this指针从 SomeClass 的 ctor 传递给线程-'B'?如果是,那么您肯定在使用虚函数时遇到了麻烦。但这与比赛条件无关。

如果您在线程-'B' 中访问全局实例变量而没有通过线程-'A' 传递它,则可能存在竞争条件。'new' 指令由大多数编译器(如 ....

pClass = // Step 3
operator new(sizeof(SomeClass)); // Step 1
new (pClass ) SomeClass; // Step 2

如果只有 Step-1 完成,或者只有 Step-1 和 Step-2 完成,则访问pClass未定义。

高温高压

于 2010-07-06T17:12:41.470 回答
0

在没有同步的情况下,vtable 上可能存在竞争条件是正确的,因为线程 A 中的构造函数对内存的写入可能对线程 B 不可见。

然而,用于线程间通信的队列通常包含同步来精确解决这个问题。因此,我希望由引用的队列getMonitorFromSharedQueuepassMonitorToSharedQueue处理这个问题。如果他们不这样做,那么您可能会考虑使用另一种队列实现,例如我在博客上写的那个:

http://www.justsoftwaresolutions.co.uk/threading/implementing-a-thread-safe-queue-using-condition-variables.html

于 2010-07-12T20:49:21.503 回答
0

我不太明白,但我认为您可能的意思有两种可能性:

A)“O”在将其传递到同步队列到“B”之前已完全构造(返回构造函数)。在这种情况下没有问题,因为对象是完全构造的,包括 vtable 指针。该位置的内存将具有 vtable,因为它都在一个进程中。

B)“O”尚未完全构造,但例如您正在this从构造函数传递到同步队列。在这种情况下,仍然必须在线程“A”中调用构造函数的主体之前设置 vtable 指针,因为从构造函数调用虚函数是有效的 - 它只会调用当前类的方法版本,不是最衍生的。因此,我也不希望在这种情况下看到竞争条件。如果您实际上是this从其构造函数中传递到另一个线程,您可能需要重新考虑您的方法,因为可能调用未完全构造的对象似乎很危险。

于 2010-07-06T17:07:31.727 回答