C++20でコルーチンが言語コアに追加された。またC++23ではライブラリ機能が追加されるだろう
C++20しか使えない現在の非同期処理の実装の実用的な選択肢として、またC++23におけるコルーチンの実装の可能性の一つとして、cppcoroを眺めていく
なぜコルーチンはC++に入ったのか
まず一般的にC++erの夢というのは大体以下のような感じである
- 雑に書いても安全に速度が出てほしい
- 速度が出てほしい
そしてそれとは別に非同期処理をやりたいという話がある
非同期処理の解決法にはthread+mutexによる解決とstateパターンによる解決の2種類があるが、それぞれ以下のような問題がある
- thread+mutexによる解決の問題
- いわゆるC10k問題など、生成数に上限がある
- lock(mutex等)は重い
- stateパターンによる解決の問題
- 書きづらい
そこでコルーチン
- c10k: コルーチンであれば大量に生成しても大丈夫
- lock: ロックフリーによる実装がそこそこ可能
- lockが走る状況になったら処理を別のコルーチンに明け渡すだけなので、簡単なCPU命令だけで完結する
- 記述がstateパターンに比べ比較的簡単
ここでC++erの夢を見直すと、コルーチンはその夢を叶えてくれそうな気がする
- 雑に書いても安全に速度が出る
- 速度が出る
なぜcppcoroか
現在C++20に入ってるコルーチンはコア機能のみで、ライブラリ的な機能は入っていない
しかしライブラリ機能がないだけならライブラリを実装すればよく、より良い解決策としては誰かが実装してくれたものを使えばよい。cppcoroはそれをかなえてくれる
またcppcoroをベースにしたC++23(?)のコルーチンの提案が出ている
まぁこれが入るかはわからないが、cppcoroっぽい何かしらがいずれC++に入るでしょう
cppcoroは未来のC++になる可能性がある
cppcoroの大事な概念
- task
- aync_mutex
- scheduler
だいたい コルーチン版future・mutex・thread_poolのことだと理解してもらえるとわかりやすい。
C#などのasyncを知っていると話が早いかもしれない
これらについて解説する
task
mutexやネット通信処理などの非同期処理を扱っていくためのコルーチン
cppcoro::task<> add_item(std::string value)
{
cppcoro::async_mutex_lock lock = co_await mutex.scoped_lock_async();
values=42;
}
これをwhen_all (すべて実行していってすべて完了したら処理を戻す)などを使って非同期実行をやっていく
// get_recordはネット越しにデータを取ってくる関数とする
auto [task1, task2, task3] = co_await when_all_ready(
get_record(123),
get_record(456),
get_record(789)
);
async_mutex
コルーチン版mutex
std::mutexと同様に複数のthreadsから同じリソースに対し同時にアクセスされる場合、待ち処理をする
std::mutexと違うのは待ち処理を行う際、OSのlockを獲得せずコルーチンをに中断する点にある
動作原理
mutex内部に状態としてロックフリーstackがある
待ち処理が発生する場合stackに追加
mutexのlockが解除され次第stackにあるコルーチンをとりだし実行する
{
auto lock = co_await mutex.scoped_lock_async();
values = 42;
}//unlock
scheduler
UI threadやthread_poolなどなど適切なthreadでtaskを実行する
co_await schedule_on(thread_pool, get_value());
例えば「f(X)
をthread_poolの複数のスレッドで実行する」は次のようなコードになる
auto [task1, task2, task3] = co_await when_all_ready(
[]()->cppcoro::Task<int>{co_await thread_pool; return f(1);},
[]()->cppcoro::Task<int>{co_await thread_pool; return f(2);},
[]()->cppcoro::Task<int>{co_await thread_pool; return f(3);}
);
まとめ
未来がどうなるかはわからないけど未来っぽいものはgithub からcloneできるぞ
$ git clone https://github.com/lewissbaker/cppcoro.git
おまけ
概念的にはそこまで重要じゃないしC++23で入る気もあんまりしないんだけど頑張って調べたから聞いてよ
###メッセージキュー支援について
LMAX disruptor にインスパイアされたbarrier制御がcppcoroにはある
LMAX disruptor とは?
- LMAX Exchange社が開発したjavaの 高速なbloking queueライブラリ
- LMAX Exchange社は金融系の企業で、そこが「なによりもメッセージングがくっそ遅い」という理由で汎用性を残しつつゴリゴリに高速化OSS化した
- 金の力ってスゲー
高速化の秘密
- リングバッファの活用によるmalloc回数の削減
- ロックフリー
- より少ないatomic操作
- キャッシュラインの工夫によるfalse sharingの回避
barrierとは?
- 特定のリソースへのアクセスを制御する機構
- 「シングルthreadによる書きこみ」→「書き込まれるまでwait」→「複数threadによる読み込み」の明確な順序付けを連続で行える機構
- std::barrier でググると幸せになれるかも
sequence_barrierとは?
- indexでやっていくbarrier(コルーチン用)
- これがあると無限長(2**64)のバッファに対し「index iまで書き込み済み」「iに書き込まれるまで待機」みたいな制御が簡単安全高速にできる
[single|multi]_producer_sequencer
- sequence_barrirを2つ使って「書き込み済み」「書き込まれるまで待機」「読み込み済み」「読み込まれるまで待機」を制御することでsequence_barrierをリングバッファに適用できるようにする
- これらを使うとそれなりに簡単に安全高速にメッセージングを実現できる
- blockingはキューが枯渇したとき・キューがいっぱいになったときのみ起きるる。したがってリングバッファのサイズが十分であればblockingは比較的発生しづらくなる