0

vtable動的ディスパッチング実装(vtableポインターが仮想メソッドを持つオブジェクトの非表示メンバーとして格納されている)の仮想メソッド呼び出しを含む特定のC ++マルチスレッド状況で、競合状態が発生する可能性があるのではないかと疑っています。これが実際に問題であるかどうかを確認したいのですが、参照のフレームを想定できるように、boostのスレッドライブラリを指定しています。

オブジェクト「O」にboost::mutexメンバーがあり、そのコンストラクタ/デストラクタとメソッド全体がスコープロックされているとします(Monitorの同時実行パターンと同様)。スレッド「A」は、外部同期なしで(つまり、他のスレッドと同期できる「新しい」操作を囲む共有ミューテックスなしで)ヒープ上にオブジェクト「O」を構築します。ただし、「内部」がまだ存在することに注意してください。 、そのコンストラクタのスコープをロックする「監視」ミューテックス)。次に、スレッドAは、同期されたメカニズム(たとえば、同期されたリーダー/ライターキュー)を使用して、「O」インスタンス(作成したばかり)へのポインターを別のスレッド「B」に渡します(注:へのポインターのみオブジェクト自体ではなく、オブジェクトが渡されます。建設後、

スレッド「B」は、同期されたキューからオブジェクト「O」のポインタ値を読み取り、その後すぐにキューを保護しているクリティカルセクションを離れます。次に、スレッド「B」はオブジェクト「O」に対して仮想メソッド呼び出しを実行します。ここで問題が発生する可能性があると思います。

動的ディスパッチの[かなりありそうな]vtable実装での仮想メソッド呼び出しについての私の理解は、オブジェクトの非表示メンバーとして格納されているvtableポインターを取得するために、呼び出しスレッド「B」がポインターを「O」に逆参照する必要があるということです。 、およびこれはメソッド本体に入る前に発生します(当然、実行するメソッド本体は、オブジェクト自体に格納されているvtableポインターにアクセスするまで、安全かつ正確に決定されないため)。前述のステートメントがそのような実装に当てはまる可能性があると仮定すると、これは競合状態ではありませんか?

vtableポインターは、メモリの可視性を保証する操作(つまり、オブジェクト「O」のメンバー変数ミューテックスの取得)の前に、スレッド「B」によって(ヒープ内にあるオブジェクト「O」へのポインターを参照解除することによって)取得されるためです。 、それでは、「B」が、「A」がオブジェクト「O」の構造に最初に書き込んだvtableポインタ値を認識するかどうかは定かではありません。(つまり、代わりにガベージ値を認識し、未定義の動作を引き起こす可能性がありますよね?)。

上記が有効な可能性である場合、これは、スレッド間で共有される排他的に内部同期されたオブジェクトで仮想メソッド呼び出しを行うことが未定義の動作であることを意味しませんか?

また、同様に、標準はvtableの実装に依存しないため、仮想呼び出しの前にvtableポインターが他のスレッドに安全に表示されることをどのように保証できますか?コンストラクター呼び出しと、少なくとも各スレッドでの最初の仮想メソッド呼び出しを外部で同期(たとえば、「共有ミューテックスlock()/ unlock()ブロックで囲む」のように「外部」)できると思います。しかし、これはひどく不調和なプログラミングのようです。

したがって、私の疑いが真実である場合、おそらくより洗練された解決策は、メンバーのミューテックスをロックし、その後仮想呼び出しに転送するインラインの非仮想メンバー関数を使用することです。しかし、それでも、コンストラクターがコンストラクター本体自体を保護する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ポインターがコンストラクターのロックされたmutexブロック内に書き込まれるようにコードを生成します。

たとえば、各コアに専用のキャッシュがあるマルチコアシステムの状況を考えてみます。この記事によると、キャッシュは一般的に積極的に最適化されており、キャッシュコヒーレンスを強制しているにもかかわらず、同期プリミティブが含まれていない場合は、キャッシュコヒーレンスに厳密な順序付けを強制しません。

おそらく私は記事の意味を誤解していますが、それは構築されたオブジェクトへのvtableポインターの「A」の書き込み(そしてこの書き込みがコンストラクターのロックされたミューテックスブロック内で発生するという兆候がない)を意味するのではありません「B」がvtableポインターを読み取る前に、「B」によって認識されませんか?AとBの両方が異なるコア(core0の「A」とcore1の「B」)で実行される場合、キャッシュコヒーレンスメカニズムは、core1のキャッシュ内のvtableポインター値の更新(一貫性を保つ更新)を並べ替えることがあります。 「A」が書き込んだcore0のキャッシュ内のvtableポインターの値を使用して、「B」の読み取り後に発生するようにします...記事を正しく解釈している場合。

4

4 に答える 4

0

よくわかりませんが、2つの可能性があります。

A)「O」は完全に構築され(コンストラクターが返され)、同期キューに「B」に渡されます。この場合、オブジェクトはvtableポインターを含めて完全に構築されているため、問題はありません。その場所のメモリはすべて1つのプロセス内にあるため、vtableがあります。

B)「O」はまだ完全には構築されていませんが、たとえばthis、コンストラクターから同期キューに渡されています。この場合、コンストラクターから仮想関数を呼び出すことが有効であるため、コンストラクターの本体がスレッド「A」で呼び出される前に、vtableポインターを設定する必要があります。これは、現在のクラスのバージョンのメソッドを呼び出すだけです。最も派生したものではありません。したがって、この場合も競合状態が発生することはないと思います。実際にコンストラクター内から別のスレッドに渡す場合は、this完全に構築されていないオブジェクトを呼び出すのは危険であると思われるため、アプローチを再検討することをお勧めします。

于 2010-07-06T17:07:31.727 に答える
0

私があなたのエッセイを理解しようとすると、あなたはこれを尋ねていると思います:-

スレッド「A」は、外部同期なしでヒープ上にオブジェクト「O」を構築します

// global namespace
SomeClass* pClass = new SomeClass;

同時に、スレッド「A」が上記のインスタンスをスレッド「B」に渡すと言っています。これは、インスタンスが完全に構築されていることを意味します。または、SomeClass の ctor からスレッド 'B' にポインターSomeClassを渡そうとしていますか? thisはいの場合、仮想関数に関して間違いなく問題があります。しかし、これは競合状態とは何の関係もありません。

スレッド「A」が渡さずにスレッド「B」でグローバル インスタンス変数にアクセスすると、競合状態が発生する可能性があります。「新しい」命令は、....のようなほとんどのコンパイラによってレイアウトされます。

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

Step-1 のみが完了した場合、または Step-1 と Step-2 のみが完了した場合、アクセスpClassは未定義です。

HTH

于 2010-07-06T17:12:41.470 に答える
0

同期がない場合、スレッド A のコンストラクターによるメモリへの書き込みがスレッド B に表示されない可能性があるため、vtable で競合状態が発生する可能性があることは間違いありません。

ただし、スレッド間通信に使用されるキューには通常、この問題に正確に対処するための同期が含まれています。したがって、によって参照されるキューがこれを処理することを期待しgetMonitorFromSharedQueueますpassMonitorToSharedQueue。そうでない場合は、別のキュー実装を使用することを検討してください。たとえば、次のブログで書いたものです。

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

于 2010-07-12T20:49:21.503 に答える
0

暗黙的なキャッシュを使用する共有メモリ マルチプロセッサ システムでは、メイン メモリへの変更を他のキャッシュから見えるようにするためのメモリ バリアが必要です。一般に、OS 同期プリミティブ (およびそれらの上に構築されたもの) の取得または解放には完全なメモリ バリアがあり、同期プリミティブを取得 (または解放) する前に発生したすべての書き込みは、取得後にすべてのプロセッサに表示されると想定できます。それ(またはリリース)。

特定の問題については、内部にメモリバリアがMonitor::Monitor()あるため、返されるまでに vtable は少なくとも に初期化されていますMonitor::vtable。から派生した場合は問題が発生する可能性がありますがMonitor、投稿したコードではそうではないため、問題ではありません。

呼び出し時に正しい vtable を確実に取得したい場合は、 をgetMonitorFromSharedQueue()呼び出す前に読み取りバリアを設定する必要がありますif->dynamicCall()

于 2010-07-06T19:00:28.000 に答える