yohhoyの日記

技術的メモをしていきたい日記

Read-Copy Update @ C++26

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
    • Readerロック管理とメモリ回収機構を提供するクラス。Readerロック取得/解放操作のためにLockable要件を満たす。*2
    • rcu_domain::lockは同一スレッド上での再帰的Readerロックをサポートする。*3
    • C++2c標準ライブラリではシステムグローバルなstd::rcu_default_domain()のみが提供される。
  • 下記3パターンの実装方式に対応したAPIが提供される。
    • 侵襲(intrusive) RCU:データ型Tstd::rcu_obj_base<T>から継承*4し、retire()メンバ関数を利用する。
    • 非侵襲(non-intrusive) RCU:std::rcu_retire(p)関数を利用する。
    • 同期(synchronous) RCU:std::rcu_synchronize()関数+手動メモリ解放を行う。
  • 全retire操作の完了待ち:std::rcu_barrier()関数
  • retire操作のメモリ解放処理はカスタマイズ可能。デフォルトstd::default_delete<T>
  • プロダクション品質の実装例として facebook/folly ライブラリが存在する。機能的にはC++2c標準ライブラリの上位互換相当。*5

関連URL

*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."