C++2c(C++26)標準ライブラリに追加される<rcu>ヘッダについて。Read copy updateの略。
// C++2c #include <atomic> #include <mutex> // scoped_lock #include <rcu> struct Data { int m1; /*...*/ }; std::atomic<Data*> data_; // new確保された初期値が別途設定される前提 void multiple_reader() { // Readerロック取得(ノンブロッキング操作) std::scoped_lock rlk(std::rcu_default_domain()); // 現在の共有データへのポインタ取得 Data* p = data_.load(std::memory_order_acquire); // アドレスloadにはmemory_order_consume指定が想定されていたが、 // C++20以降はconsume指定はdiscourage扱いとなっている(P0371R1)。 // acquire指定セマンティクスはconsumeを完全に包含する。 int m1 = p->m1; } // Readerロック解放 void single_writer() { // 新データを作成 Data* newdata = new Data(); newdata->m1 = /*...*/; // ポインタをアトミック更新 Data* olddata = data_.exchange(newdata, std::memory_order_release); // 全Readerロック解放後のデータ回収をスケジュールする std::rcu_retire(olddata); // 内部実装には何らのスケジューラの存在が仮定されている。 // ポインタはstd::default_delete<Data>経由でdeleteされる。 // 実装によっては呼出スレッド上にて待機+deleteされる可能性あり。 }
まとめ:
- プリミティブなスレッド間同期機構として、ユーザ空間RCU(Userspace Read-Copy Update)を提供する。
- RCU機構で実現される制御はReader-Writerロック*1と似ているが、Readerロック取得はブロッキングされず高いスケーラビリティを持つ。
- Writerスレッドは共有データを直接書き換えるのではなく、旧データから新データへとコピー&書き換えたのち、“共有データを指すポインタ” が指す先を旧データから新データへとアトミック更新する。
- 全体としては新旧データが同時に存在する状態となり、以降のReaderロック取得スレッドは新データを参照する。全Readerスレッドが旧データを読取り終わった後に、旧データのメモリ領域を回収(reclaim)しなければならない。
- RCU機構が直接提供するのは旧データのメモリ解放タイミング制御のみ。
- RCUドメイン
std::rcu_domain
- 下記3パターンの実装方式に対応したAPIが提供される。
- 全retire操作の完了待ち:
std::rcu_barrier()
関数 - retire操作のメモリ解放処理はカスタマイズ可能。デフォルト
std::default_delete<T>
- プロダクション品質の実装例として facebook/folly ライブラリが存在する。機能的にはC++2c標準ライブラリの上位互換相当。*5
関連URL
- (PDF) P2545R4 Read-Copy Update (RCU)
- (PDF) Unraveling RCU-Usage Mysteries
- Stupid RCU Tricks: CPP Summit Presentation: paulmck — LiveJournal
- Read, Copy, Update... Then What, CppCon 2017
- The Upcoming Concurrency TS Version 2 for Low-Latency and Lockless Synchronization, CppCon 2021
- Lock-free Data Structures. The Inside. RCU
- userspace RCU(QSBR)の使い方と解説 - くまメモ
- [C++]WG21月次提案文書を眺める(2022年02月) - 地面を見下ろす少年の足蹴にされる私
*1:C++標準ライブラリの std::shared_mutex、POSIXの pthread_rwlock_* など。
*2:データ読取り操作を行うRCU保護区間(region of RCU protection)の開始/終了を、std::scoped_lock や std::unique_lock によるScoped Lockイディオムで実現するのために lock/unlock メンバ関数が提供される。
*3:C++標準 std::shared_mutex では再帰的なロック操作をサポートしない。
*4:いわゆるCRTP(Curiously Recurring Template Pattern)継承関係。
*5:P2545R4 "A near-superset of this proposal is implemented in the Folly RCU library."