C++ のユニバーサル I/O 抽象化

この記事は、C++ のユニバーサル非同期抽象化の続編です。この記事では、C++23 を対象とした Executor の提案について説明しています。その後、かなりのことが起こりました。

SG-11 、並行性と並列処理のすべてを担当するスタディ グループが前進し、提案を LEWG に送りました - C++23 ドラフトに将来の改訂版が含まれることを期待しています。

53 の分割 63 に と 72 現在、論文の対象となっています。これはパズルの非常に重要なピースであり、来月プラハで議論されるのを楽しみにしています.

エグゼキュータの簡単な歴史については、このホワイト ペーパーで読むこともできます。

最後に、おそらくもっと重要なこととして、Facebook は libunifex と呼ばれるセンダー/レシーバーとスケジューラーのオープンソース実装を公開しました。これは P0443 の正確な実装ではなく、より多くの機能とアルゴリズムを備えていますが、同じ基本設計とアーキテクチャを実装しています残念ながら、まだ概念を使用していないので、愚かにも C++20 ライブラリの実装を試み続けています。幸いなことに、コルーチンは GCC にマージされ、コンセプトは clang にマージされたため、エグゼキュータの提案を実装できる多くのコンパイラが存在するようになりました.

エキサイティングな時代。

前回は、2 つの基本的な概念について説明しました:

  • 85 特定のコンテキスト (スレッド プールなど) で操作をスケジュールできる概念
  • 95 特定のコンテキスト (スレッド プールなど) で関数を実行できるようにする概念。 113 のような概念に値するものではありませんでした 単純に 128 の CPO になる可能性があります 2 .

スレッドなどの実行コンテキストでコードを実行できることは素晴らしいことですが、後でコードを実行したい場合はどうすればよいでしょうか?コードの一部を 5 分ごとに実行する必要があるかもしれません:

void child() {
 while(true) {
 fmt::print("Are we there yet?");
 this_thread::sleep(5min);
 }
}
int main() {
 scheduler auto s = /*...*/
 execution::execute(s, as_receiver(child));
}

これでうまくいきます3 .しかし、そのスレッドでは他に何も実行されず、リソースの使用率がかなり低くなります.スレッドはプロセスよりも安価ですが、それでも作成に時間がかかります.数千のタスクがある場合は、タスクごとに 1 つのスレッドを使用しないでください.

必要なのは タスク です スレッドではなく 5 分間中断されます。

実際、スレッドをアイドリングして、タスクを待機させる必要がある場合は数多くあります。

  • 眠っている
  • ソケットまたはファイルからデータが読み込まれるのを待っています
  • デバイスがフラッシュされるのを待っています
  • プロセスが完了するのを待っています

これらの操作はすべて「I/O」と呼ばれ、カーネルを備えたプラットフォームでは通常、カーネルによって処理されます。

139 を呼び出す場合

このダンスには代償があります。かなり小さいもので、気付くには数百または数千のスレッドを作成する必要があります。コストのほとんどは、おそらくコンテキスト スイッチ自体ではなく、キャッシュの無効化によるものです。

カーネルにスケジューリングを行わせる代わりに、ユーザー空間でスケジューリングを行うシステム API があります。

基本原則はかなり単純です:

  • ファイル記述子またはハンドルでデータが利用可能になったときに通知するようにカーネルに要求します
  • どちらか
    • 別のスレッドで、少なくとも 1 つのリクエストが完了するまで待ちます
    • リクエストが完了したことを定期的に確認する
  • リクエストに関連付けられたコールバックを実行する

非同期 I/O API

リアクター:select、poll、epoll

これらの POSIX (148 は Linux 固有のものです) API にはさまざまな動作がありますが、ここで取り上げる価値はありません。Julia Evans がそのトピックについて私よりもうまく説明したからです。

ただし、原則は同じです:

  • タスクが監視したいファイル記述子を登録する
  • 他のタスクを実行する
  • API を呼び出します (つまり、151 を呼び出します) そのファイルのセットで)
  • 少なくとも 1 つのファイル記述子の読み取りまたは書き込みの準備が整うまでブロックします
  • 読み取り準備が整ったファイルに関連付けられた継続 (コールバック) を呼び出します
  • 十分なデータが利用可能な場合、必要なノンブロッキング読み取りを実行します
  • すべてのコールバックが実行されるまで繰り返す

これは、単一のスレッド (プログラムがファイル記述子イベントの待機を開始する前に一部のタスクがキューに入れられる) で発生するか、複数のスレッドで発生する可能性があり、その場合はファイル登録を同期する必要があります。詳細は後述します。

この一般的なワークフローは reactor です パターン。

プロアクター:AIO および IOCP

リアクターの問題の 1 つは、161 ごとに たとえば、ファイルの操作では、次のことを行う必要があります:

  • ファイルを登録します (1 syscall)
  • ある程度まで投票する データが利用可能です (1 syscall)
  • 十分なデータが得られるまで繰り返す
  • データを読み取ります (ノンブロッキング方式で) (1 syscall)

システム コールは相対的に そのため、十分なデータが得られる前にタスクを再開します。この問題を緩和するために、174 などのより最新の非同期 I/O API を使用します。 (POSIX) または IOCP (Windows) は、ポーリング操作と読み取り操作をマージします。

これにより、より簡単なワークフローが可能になります:

  • ファイル ディスクリプタとバッファ セットを登録する
  • 他のタスクを実行する
  • 1 つ以上の I/O リクエストが完了したことを一時停止または定期的に確認する
  • 完了したリクエストに関連付けられた継続 (コールバック) を呼び出します
  • すべてのコールバックが実行されるまで繰り返す

これにより、syscall の数が減り、必要な I/O が完了した場合にのみタスクを再開できます。内部的に、カーネルは I/O 操作を実行するために独自の作業スレッドのプールを生成する場合がありますが、真に解放されるものはありません。より多くのシステム コールを実行するよりもはるかに効率的です。このワークフローは推進者です。 パターン。

しかし (常にありますよね?) 人々は長い間 Windows で非同期 I/O を行ってきましたが (Windows でのファイル操作が非常に遅いためかもしれません)、188 Linux では不要 (同期 I/O は十分に高速) または不適切 (遅延が大きすぎる) と見なされます。実際、198 Linux ではユーザー空間に実装されていますが、同様のカーネル API 200 代わりに使用できます。いずれにせよ、これらの API はファイル I/O を処理するように設計されており、212 のようにソケットに使用することはできないか、推奨されません。 すべてのケースでパフォーマンスが向上します。

C++ の方が興味深いかもしれませんが、ファイルとソケットの両方をまとめて処理する効率的なインターフェイスを設計することは不可能だと人々は信じています。 とAFIO 221 などの一般的な非同期システムではなく、さまざまなインターフェイスを持つさまざまなプロジェクトとして またはトキオ。

ビヨンセは、気に入ったら指輪をはめるべきだと言った4 .まあ、私は送信者/受信者と、標準的な汎用でありながら効率的なI/Oスケジューラのアイデアがとても好きです。具体的には、237 .

io_uring

240 は、Linux カーネルのエキサイティングな新機能であり、(バッファありおよびバッファなしの) ファイル I/O やソケットなどの他のデバイスに対しても同様に機能する、非常に効率的な非同期フレームワークの設計を可能にします。253 Linux 5.15 に追加されました 267 の代わりとして と 271 、しかしそれ以来、ソケットのサポートが改善されました。これは非常に優れているため、一般的な非同期システム コール インターフェイスに変化する可能性があります。

280 は、カーネル間で共有される 2 つのキュー (送信用と完了用) に基づいています。カーネルは送信キューから読み取ることができますが、カーネルが書き込みを行っているときでも、アプリケーション スレッドは完了キューから読み取ることができます。

キューは、ロックフリーの単一コンシューマー、単一プロデューサー リングです (名前の由来)。Linux 5.5 以降、カーネルは、完了キューにスペースができるまで完了を保持するオーバーフロー リストを維持します。

同様に、アプリケーションは送信キューがオーバーフローしないように注意する必要があります。送信キューには、一度に 1 つのスレッドしかアクセスできません6 。 .

作業がリングに追加されると、単一のシステム 298 呼び出しを使用して、送信キューにすべての新しい作業を送信し、エントリが完了キューに追加されるのを待つことができます。

以下は、I/O スレッドの疑似実装です:

void io_context::run() {
 io_uring ring;
 io_uring_queue_init(URING_ENTRIES, &ring, 0);
 struct io_uring_cqe* cqe;
 while(true) {
 add_pending_operations_to_io_uring();
 io_uring_wait_cqe(&ring, &cqe); // single syscall to submit and wait
 auto* operation = operation_from_completion(cqe);
 io_uring_cqe_seen(&ring, cqe);
 execute_completion(cqe);
 }
 io_uring_queue_exit(&m_ring);
}

このスライド コードは、非常に低レベルのユーザー空間リング管理を処理する liburing ライブラリを特徴としています。

304 それぞれが独自のリングを持つ複数のスレッドで実行できます。ただし、各キューは一度に 1 つのスレッドからのみアクセスできます。さらに、316 名前がブロッキング コールを示唆しているように、キューに作業を追加するにはどうすればよいでしょうか?

まず、オペレーションを送信キュー バッファにプッシュするためのスレッド セーフな方法が必要です7 。 上の図では緑色の四角形で表されています。

class io_context {
 std::mutex mutex;
 intrusive_queue<operation*> pending;
 void start_operation(operation* op) {
 std::unique_lock _(mutex);
 pending.push(op);
 }
};

ただし、I/O スレッドが現在 326 でブロックされている場合 ,要素をキューに追加したことをどのように確認できますか?

単純な解決策は 335 を使用することです しかし、これにはいくつかの問題があります:

  • 346 の出入り 処理によってシステム コールとコンテキスト スイッチが発生し、より一般的には CPU サイクルが浪費されます。
  • タイムアウトの値によっては、レイテンシが増加し、操作が開始されてからカーネルが i/o リクエストの実行を開始するまでに遅延が発生します。

代わりに、io/thread のダミー ファイル ハンドルで読み取り操作をスケジュールし、送信側スレッドでそのファイル記述子に書き込むことができます。これにより、354 が発生します。 戻る。

Linux では、362 を使用できます。 、私が知る限り、それはその小さなダンスを行うための最も効率的な方法です.

class io_context {
 std::mutex mutex;
 std::queue<operation*> pending;
 int fd = ::eventfd(0, O_NONBLOCK);
 eventfd_t dummy;
 void run() {
 schedule_notify();
 while(true) {
 // --
 io_uring_wait_cqe(&ring, &cqe);
 if(cqe->user_data == this) {
 schedule_notify(); // re-arm
 }
 //...
 }
 }
 void schedule_notify() {
 auto sqe = io_uring_get_sqe(&m_ring);
 io_uring_prep_poll_read(sqe, fd, &dummy, sizeof(dummy));
 io_uring_set_data(sqe, this);
 }
 void start_operation(operation* op) {
 std::unique_lock _(mutex);
 pending.push(op);
 eventfd_write(fd, 0); // causes io_uring_wait_cqe to return
 }
};

作業をキューに入れるこのメカニズムは、374 に固有のものではありません 389 でも使用できます 、 392402 など

ポーリング

キューに通知して完了イベントを待機するこの方法では、数十万の IOPS の後で目に見えるようになるオーバーヘッドが発生します。PCI4/PCI5 などの新しい標準、および対応するドライブとネットワークでは、これは問題には見えないかもしれません。ハードウェア、I/O が CPU バウンドになり始め、カーネルがボトルネックになります。

この趣旨で、415 一部のユースケースで非常に高いスループットを可能にするポーリング モードを提供します。標準でそのようなモードをサポートするための P2052 提唱者。

最も単純な I/O 操作:schedule_at

A Universal Async Abstraction for C++ では、426 について説明しました。 特定のスケジューラに関連付けられた実行コンテキストで操作を実行するアルゴリズム

oneway_task do_something(execution::scheduler auto s) {
 co_await execution::schedule(s);
 fmt::print("Hello"); //runs in the context associated to the scheduler s
}

io コンテキスト、別名実行コンテキストを理解したので、436 を追加できます。 444 へのパラメータ 8 アルゴリズム.P1031 から締め切りのアイデアを盗みました - 低レベルのファイル I/O ライブラリ.これは、相対的または絶対的な時間を表すことができる単純なユーティリティです

task annoying_child(execution::scheduler auto s) {
 while(true) {
 //Suspend the task for 5 minutes,
 //The thread is free to do something else in the meantime
 co_await execution::schedule(s, 5min);
 fmt::print("Are we there yet?");
 }
}

ここでは、459 前回 462 で見たように、送信者を返します 唯一の違いは、471 メソッドは、カーネルによってスケジュールされているタイムアウト「i/o」操作につながります。

488 タイムアウトのサポートが組み込まれています。他のスケジューラーは 494 を使用する場合があります または 505

タイマーに加えて、ほとんどの非同期 API のサポート:

  • さまざまなモードでのファイル記述子 (ファイル、ソケット、パイプ、その他の「ファイルのような」オブジェクト) の読み取り、書き込み
  • ファイル記述子からのポーリング (データを実際に読み取らずに待つ)
  • ファイル記述子を開く、同期する、閉じる
  • リモート ソケットへの接続と接続の受け入れ

などの低レベル API を想像することは可能ですが、

auto read_file(scheduler, native_handle, buffers) -> read_sender;
auto close_file(scheduler, native_handle) -> close_sender;

代わりに、519 などの io オブジェクトがほとんど得られない可能性が高くなります。 s と 524

template<execution::scheduler scheduler = std::default_scheduler>
class file;

task read_data(execution::scheduler auto s, buffers & buffs) {
 file f(s);
 co_await f.open("myfile.txt");
 co_await f.read(buffs);
 co_await f.close();
}

535 の理由が気になるなら P1662 を読んで泣いてください。

スレッドは共有リソースです

ハードウェア スレッドの数には制限があり固定されており、RAM とは異なり、それ以上ダウンロードすることはできません。

したがって、理想的には、プログラムはアクティブなスレッドとほぼ同じ数の頻繁にアクティブなスレッドを使用する必要があります。

残念ながら、独立したライブラリは独自のスレッドとスレッド プールを使用する場合があります。ほとんどすべてのグラフィックス フレームワークと同様に、I/O ライブラリは独自の偶数ループを作成する場合があります。

標準ライブラリは、並列アルゴリズムと 548 のために内部でスレッドを使用します .一部の実装では、550 ごとに開始されるスレッドがあります。 呼び出し (564 を行う多くの理由の 1 つ) ひどいです)。

ベクトルの 1000 要素を一度に変換できますが、1000 ベクトルの 1000 要素を同時に 1000 回変換するのは困難です。または何か。

これが、P2079 - エグゼキュータの共有実行エンジンが、グローバルにアクセス可能な 実行 を主張する理由です。

私はその論文が好きですが、本当に必要なのはグローバルにアクセス可能な io コンテキスト です .より具体的には、グローバルにアクセス可能な io スケジューラ .

実行コンテキストの厳密なスーパーセットである I/O コンテキスト。

この顔 😵 (これは正しい顔ではないかもしれません) を作成する前に、シングルトンを標準に追加するという考えに混乱して恐怖を感じる前に、一部のプラットフォームがずっと前に同じ結論に達し、グローバルな I/O コンテキストを公開していることに注意してください。すべてのアプリケーション:

  • Windows スレッド プールは、作業 (io 要求を含む) を送信できる既定のスレッド プールを公開します。これは、Microsoft の STL 実装で使用されます。
  • Apple プラットフォームには Grand Central Dispatch があります。これは同様に機能しますが、はるかにクールな名前です。

他の POSIX プラットフォームには、これに相当するデファクト ソリューションはありません。また、1 スレッドのコンテキストは十分に単純ですが、ユーザー空間のスケジューリングは依然としてスケジューリングであり、スケジューリングは困難です。

576 など、Linux で使用できるライブラリがいくつかあります。 または 584 、または実装者がスクラッチのために何かを調理することができます.

キャンセルおよび停止トークン

C++ のエラー管理は単純で解決済みの問題と見なされます9 物事を盛り上げるために、非同期性は 3 つ目のチャネル、キャンセルを追加します。実際、キャンセルはエラーではありません10 。 .

ただし、キャンセルの処理について説明する前に、キャンセル要求の送信について説明しましょう。通常、タスク全体または操作をキャンセルすると、後続の操作のチェーン全体がキャンセルされます。

sequence(read(stdin, buffer), write(stdout, buffer))

たとえば、ここで読み取りをキャンセルすると、書き込みは実行されません。[P1677] で述べたように、キャンセルは関数から早期に戻る非同期バージョンです。

591 これは 606 と同時に受け入れられた C++20 の機能です。 11

死とすべての良い話のように、非同期キャンセルには 3 つの要素があります。

  • 611
  • 620
  • 636

これは、C# の CancellationToken や Javascript の AbortController と同じ考え方に基づいています。

642 トークンを作成できます、656 663 があります 一度だけ true を返すメソッド 674 さらに、689 のときにコールバックを自動的にトリガーできます。

同じ 694 に関連付けられたすべてのトークンとコールバック 同じスレッドセーフな ref-counted 共有状態を共有します。 複数のスレッドがある場合、それ自体がスレッドセーフです。)

すでに GCC に実装されているため、コンパイラ エクスプローラで操作できます


#include <stop_token>
#include <cstdio>

int main() {
 std::stop_source stop;
 auto token = stop.get_token();
 std::stop_callback cb(token, [] {
 std::puts("I don't want to stop at all\n");
 });
 std::puts("Don't stop me now, I'm having such a good time\n");
 stop.request_stop();
 if(token.stop_requested()) {
 std::puts("Alright\n");
 }
}

トークンは、適切なタイプ 12 のコルーチン タスクにアタッチできます。 または受信機に取り付けられます。

カスタマイズポイント 715 その後、実行コンテキストで使用して、操作をキャンセルするかどうかを問い合わせることができます。

操作は、実行される予定の実行コンテキストでキャンセルする必要があります。

実行中の I/O 操作の場合、リクエストをカーネルに送信してリクエストをキャンセルできます (728 Windows では 738742 など)。タイマー、ソケット読み取り、または他の方法では決して完了しない可能性のあるその他の操作をキャンセルする場合に特に重要です。

実行コンテキストの有効期間

ある時点で、停止トークンを使用して実行コンテキストを停止し、実行中のすべてのタスクをキャンセルしました。とても便利でした。

つまり、残念ながら、タスクをキャンセルすると、破棄された可能性のある実行コンテキストでタスクが再スケジュールされたり、別のタスクがスケジュールされたりする可能性があるという災害のレシピです。ルイス!)

代わりに、そのコンテキストで他の操作を実行またはスケジュールする可能性のあるすべての操作が完了するまで、実行コンテキストを破棄しないでください。

これは 756 で実現できます エグゼキュータに関する最初のブログ投稿で言及したアルゴリズム.

レシーバーとコルーチンの非対称性

ただし、バラバラではありません。センダー/レシーバーと awaitables/continuations の間には、いくつかのミスマッチがあります。

レシーバーには 3 つのチャネルがあります。set_value、set_error、set_done は、それぞれ成功、失敗、キャンセルを表します。

コルーチンには戻り値 (単一の型 - レシーバーは複数の値の型をサポートする P1341) があり、例外を再スローできます13 .

レシーバーのマッピングは、いくつかの方法で実現できます。

<オール> <リ>

ある種の 761 を返す

task example() {
 inspect(auto res = co_await sender) {
 <cancelled_t>: {

 }
 res.success():{

 }
 res.failure(): {

 }
 };
}

上記の例はパターン マッチングを示していますが、型と式の両方のマッチャーを混在させることができるかどうかはわかりません。

型を使用して成功と失敗を区別することはできません。それらは同じ型を持つ可能性があるためです。

  1. 例外を使用してエラーとキャンセルの両方を伝播する
task example() {
 try {
 co_await sender;
 }
 catch(const std::error_status&) {/*...*/}
 catch(const std::cancelled_operation&) {/*...*/}
}

これにはいくつかの問題があります:

    <リ>

    セマンティック - 例外を使用してキャンセルを通知すると、キャンセルがエラーのように見えますが、そうではありません。なんて素晴らしい!

    <リ>

    パフォーマンス - 例外への依存により、必要なヒープ割り当てが十分に悪くないかのように、組み込みプラットフォームでの使用がさらに難しくなります!パフォーマンス以外に、例外のサポートそのものが欠けている場合があります。

しかし実際には、コルーチンは異なる結果を報告するために例外を使用する必要はありません.これはコルーチンの簡略図です.コルーチンは中断され、継続ハンドルによって表される特定のポイントで再開されます.

操作の結果に応じて、再開する可能性のあるいくつかの継続を持つコルーチンを想像できます。

これはレシーバーのより良いモデル化であり、例外のパフォーマンスと実装可能性の問題に苦しむことはありません (より多くの 770 を犠牲にして) 追跡します。)

とにかく…これはブログ投稿主導のデザインになりました…

つまらない言語について話しましょう。Go について話しましょう。

ゴルーチン14 ゴルーチンではありません

Go プログラミング言語の機能であるゴルーチンは、スタックフルであるだけでなく、再開メカニズムとスケジューリング メカニズムの両方をモデル化するという点で、C++ コルーチンとは大きく異なります。Go は、組み込みの I/O およびコルーチン スケジューラを提供します。これは、I/O を実行するときにゴルーチンを中断するプログラムに代わって処理し、ロックまたはその他のブロッキング操作を取得しようとします。

C++ コルーチンはゴルーチンではありません。 C++ コルーチンは非同期性を意味するものではなく、スケジューリングは言うまでもありません .C++ は、「使わないものにはお金を払わない」というマントラに反し、多くの環境で C++ を使えなくするため、I/O スケジューラを組み込むような言語ではありません。

そうは言っても…

コルーチン、センダーレシーバー、および I/O スケジューラーの組み合わせは、ゴルーチンをエミュレートできます (つまり、スタックレス非耐性)。C++ コルーチンは、単純な同期ジェネレーターとしても使用できます。これは、はるかに一般的で拡張可能なシステムです。

最終的な目標は、ブロックする可能性のあるすべての呼び出しを非同期式にすることだと思います。 789 のように .言語に焼き込むのではなく、ライブラリ ソリューションとして。

例:791 非同期ミューテックスを実装します (804 とは異なります) のストランド)、コルーチンを再開することでロックを取得できるように:

task s::f() {
 co_await m_mutex.lock();
 // Do stuff
 m_mutex.unlock();
}

内なる Gopher にチャネリング

Goroutines に沿って、go は Go の最高の機能の 1 つであるチャネルを提供します。チャネルは、概念的には比較的単純です。書き込みは、バッファリング (書き込まれたデータが保存され、ライターはその方法で続行できます) または非バッファリング (リーダーがデータを取得する準備ができるまでライターは中断されます) のいずれかです。まあ…

using namespace cor3ntin::corio;
template <execution::scheduler scheduler>
oneway_task go_write(scheduler sch, auto w) {
 int i = 10;
 while(i) {
 co_await sch.schedule(std::chrono::milliseconds(100));
 co_await w.write(--i);
 }
}

template <execution::scheduler scheduler>
oneway_task go_read(scheduler sch, auto r, stop_source& stop) {
 while(true) {
 int value = co_await r.read();
 std::cout << "Got value " << value << "\n";
 if(value == 0) {
 stop.request_stop();
 break;
 }
 }
}

int main() {
 stop_source stop;
 io_uring_context ctx;
 std::thread t([&ctx, &stop] { ctx.run(stop.get_token()); });

 auto c = make_channel<int>(ctx.scheduler());

 go_write(ctx.scheduler(), c.write());
 go_read(ctx.scheduler(), c.read(), stop);
 t.join();
}

C++ にできないことはありません!

チャネルの実装はまだ完全には準備ができていません。この記事はすでに十分に長くなっています。チャネルの実装と、それらを実装するために必要ないくつかのユーティリティ (817 など) に戻るかもしれません。 、821 アルゴリズムと 839 カスタマイズポイント!

素晴らしい機会が待っています

2020 年になり、消費者向けの CPU でさえ 2 桁のコア数を備え、ストレージは 10 GB/秒の読み取り速度を提供し、ネットワークは増え続けるトラフィックに対応する必要があります。

これらの課題に直面して、ユーザー空間ネットワーキングを検討したり、スパゲッティ コードベースを維持するための費用がかかることに取り組んだりした人もいます。

長い間、C++ 委員会は、非同期ファイル I/O が意味をなさないか、ネットワークと根本的に両立しないと考えていたようです。ユーザビリティ (別名 ASIO および AFIO)。

インターフェイスの使いやすさほど気にするのはパフォーマンスではありません。良くも悪くも、パフォーマンスと人間工学のどちらかを選択する場合、委員会はパフォーマンスを優先する傾向があります15 .

幸いなことに、これらの分断を解決する方法がついに登場したようです:

  • 841 デバイスの種類を問わない非常に高性能な I/O を提供します。
  • Sender Receiver は、構成可能で低コストの非割り当て抽象化を提供すると同時に、非同期操作の有効期間のシンプルなメンタル モデルを提供します。
  • コルーチンは、99% のユースケースで非同期 I/O を非常にシンプルにします。

非同期ネットワークは便利です。

非同期 I/O の方が優れています。

すべてを待ちましょう!

P2052 からの引用を残します - 最新の C++ i/o を一貫した API エクスペリエンスにする.

私の意見では、送受信者は天才です。それがどれほどゲームを変えるかを人々が理解できないほど単純なことです:完全に決定論的で、超高性能で、拡張可能で、構成可能で、非同期の標準 I/O を可能にします。それは巨大です。 Rust、Go、Erlang でさえ、他の現代的なシステム プログラミング言語にはありません。 ― ナイル・ダグラス

次回まで、お気をつけて!読んでくれてありがとう。

リソースとリファレンス

カーネル レシピ 2019:Jens Axboe - 「io_uring による IO の高速化」

論文

io_uring による効率的な IO、Jens Axboe

P1897 - C++23 エグゼキュータに向けて:アルゴリズムの初期セット - Lee Howes

P1341 - 標準ライブラリでの非同期 API の統合 - ルイス ベイカー

P2006 - 基本操作として connect()/start() を使用して送信側/受信側でヒープ割り当てを排除する - Lewis Baker、Eric Niebler、Kirk Soop、Lee Howes

P1678 - コールバックとコンポジション - Kirk Shoop

P1677 - キャンセルはエラーではありません - Kirk Shoop、Lisa Lippincott、Lewis Baker 著

P2052 - 最新の C++ I/O を下から上まで一貫した API エクスペリエンスにする - Niall Douglas

P0443 - C++ の統合エグゼキュータの提案 - Jared Hoberock、Michael Garland、Chris Kohlhoff、Chris Mysen、Carter Edwards、Gordon Brown、David Hollman、Lee Howes、Kirk Soop、Eric Niebler

P2024 - ブルームバーグによる統合執行者の分析 - David Sankel、Frank Birbacher、Marina Efimova、Dietmar Kuhl、Vern Riedlin

<オール>
  • 実際にはジャック・オニールが議長を務めていないグループ。順不同で話すことを恐れて、そこに行ったことはありません。伝説によると、彼らは円卓で食事をし、フォークをめぐって争っています。 ↩︎

  • 死にたくない丘! ↩︎

  • 855 の場合 864 で防ぐことができないほどすぐには返されません 一方向の実行は貧弱な基本操作であるため ↩︎

  • Google のソフトウェア エンジニアリングで学べること:時間をかけてプログラミングから学んだ教訓、およびソフトウェア エンジニアリングに関する多くの優れた洞察。 ↩︎

  • Linux 5.6 では、再設計されたワーカー スレッドなど、多くの改善が行われます。 ↩︎

  • この文の最初のドラフトは 「送信キューは同時に 1 つのスレッドのみがアクセスできます」 です。 .しかし 870 という言葉はあまりにも微妙な言葉であり、私という人間が適切に使用することはできません。 ↩︎

  • 私が勝手につけた名前。 ↩︎

  • 私もそれを作りました。 libunifex は 880 を使用します と 897 ↩︎

  • そうではありませんし、これからもありません。 [P0709] [P1947] [P1886] [P1886] [P0824] [P1028] [P0323] ↩︎

  • P1677 - キャンセルはエラーではありません。セレンディピタスという単語が 54 回含まれているという理由だけで読む価値のある論文です。 . ↩︎

  • 900 現在、C++ でスレッドを開始する推奨される方法です - 911 を検討するのが公平だと思います どうやってこの不運な状況に陥ったかを考えてみてください。 ↩︎

  • 誰か それについてブログ記事を書く必要があります… ↩︎

  • 実際、C++20 の継続は決して 923 にはなりません。 、これはかなり残念です。 ↩︎

  • コルーチンは、10 年間の大半をコルーチンに取り組んできた Gor Nishanov の名前にちなんで、Gorroutine (R が 2 つ) と呼ばれることもあります。ありがとうゴル! ↩︎

  • それを読むときは、標準の連想コンテナについて考えないようにしてください。遅すぎる! ↩︎