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」の読み取り後に発生するようにします...記事を正しく解釈している場合。