RustProgrammingRust開発者

なぜ**Arc::make_mut**はユニークな所有権を確認する際に**Acquire**/**Release**メモリ順序を使用しなければならないのか、そして**Relaxed**順序が許可するデータ競合は何か?

Hintsage AIアシスタントで面接を突破

質問への回答

Arc::make_mutは、まずArcが割り当てに対して唯一の強い参照を保持していることを確認することによって、内部データへの変更可能なアクセスを提供しようとします。このチェックは、強い参照カウントに対してAcquire順序でのアトミックロードを使用して行われます。カウントが正確に1であれば、操作は変更可能な参照を返し、そうでなければ内部データをクローンし、新しい割り当てを指すようにArcを更新します。

use std::sync::Arc; let mut data = Arc::new(5); *Arc::make_mut(&mut data) += 1; // 共有の場合のみクローンされています

Acquire/Releaseのペアは重要です。なぜなら、別のスレッドがそのArcをドロップするとき、カウントのReleaseデクリメントを実行するからです。make_mut内のAcquireロードは、デクリメントの前にドロップするスレッドが行ったすべてのメモリ書き込みが現在のスレッドに対して可視性があることを保証し、内部データに対するデータ競合を防ぎます。

実生活の状況

設定の更新がArc<Config>を通じて伝播される高スループットのメトリクス集約サービスを考えてみてください。何千というスレッドが現在の設定を読むために参照を保持していますが、管理スレッドはサービスを再起動せずにしばしばしきい値を調整する必要があります。

単純なアプローチは、すべての読み取りのためにConfigRwLockでラップし、ロックすることや、共有に関係なくマイナーな更新のたびに全体の構造をクローンすることです。最初の解決策はキャッシュラインのバウンスとロックのオーバーヘッドに悩まされ、二番目の解決策は設定が実際にユニークなときに冗長な割り当てでメモリとCPUサイクルを無駄にします。

代替案は、ロックフリーの更新のためにAtomicPtrをハザードポインタとともに使用することですが、これは複雑な手動メモリ管理が必要で、エラーが発生しやすいです。別のオプションは、ポインタ自体のアトミックスワップを可能にするRwLock<Arc<Config>>を使用することですが、これによりポインタ交換のための余分な間接操作とロックが追加されます。

チームはArc::make_mutを選択しました。なぜなら、一般的なケースを最適化しているからです:他のスレッドが参照を保持していない場合(強いカウントが1の場合)、管理スレッドが割り当てなしでデータをその場で変更できます。設定が共有されている場合は透明にクローンされます。これは、他のリーダーの最後のArcをドロップしたとき(Releaseを使用)に、管理スレッドのその後のチェック(Acquireを使用)が設定へのすべての以前の書き込みを確認できることを保証する厳密なAcquire/Releaseセマンティクスを要求します。結果として、低い競合状態での設定更新の待機時間が40%短縮されました。

候補者が見落としがちな点

なぜArc::make_mutの参照カウントチェックにRelaxed順序を使用できないのか?

Relaxed操作は、発生順序の保証を提供しません。もしmake_mutが強いカウントが1かどうかを確認するためにRelaxedを使用した場合、別のスレッドから他のスレッドの書き込みを観察する前にカウントデクリメントを観察する可能性があります。これにより、現在のスレッドがデータを変更している間に、別のスレッドが論理的にそれを読み続けることになり、データ競合を引き起こすことになります。Acquireは、カウントが1であることを確認すると(他のスレッドのドロップのReleaseを介して同期され)、データへのすべての以前の書き込みも見ることを保証します。

Arc::make_mutの動作と手動でArcを**.clone()**してから変更することの違いは何ですか?

手動でクローンすることは、同じ割り当てを指す新しいArcを作成し、強いカウントを最低でも2に増やします。この新しいArcを通じて内部データへの変更可能なアクセスを取得することはできません。なぜならArcは変更不可の共有のみを提供するからです。Arc::make_mutは特別です。なぜなら、カウントが1であるかを確認し、そうであれば既存の割り当てに**&mut T**を提供するからです。そうでなければ、dataをカウント1の新しい割り当てにクローンし、元の共有データを変更不可のまま保ちつつ、新しいコピーのユニークな所有権を確保します。

弱いポインタ(Arc::downgrade)はArc::make_mutのユニーク性の保証にどのように影響しますか?

弱いポインタは強い参照カウントには参加しません。Arc::make_mutは強いカウントのみをチェックし、弱い参照は無視されます。しかし、割り当てがまだ存在する場合、弱いポインタは強いポインタにアップグレードできます。もしmake_mutがその場での変更を進め(強いカウントが1)、別のスレッドがその後弱いポインタをアップグレードした場合、そのアップグレードにより同じ変更されたデータを指す新しいArcが作成されます。これは、アップグレードが変更の後に発生するため安全であり、Rustのメモリモデルはアップグレードされたポインタが完全に修正された値を見ることを保証します。弱いカウントは変更を防ぎませんが、すべての強い参照が一時的にドロップされても割り当てを生き続けさせます。