負荷の高いマルチスレッドまたは分散アプリケーションの開発分野では、非同期プログラミングに関する議論がしばしば発生します。 今日は非同期の詳細を掘り下げ、それが発生したときの状態、使用するコードとプログラミング言語にどのように影響するかを学びます。 FuturesとPromisesが必要な理由を理解し、コルーチンとオペレーティングシステムに触れます。 これにより、ソフトウェア開発中に生じるトレードオフがより明確になります。
この資料は、Yandex Data Analysis Schoolの教師であるIvan Puzyrevskyの報告書に基づいています。
ビデオ録画
1.コンテンツ
2.はじめに
みなさん、こんにちは、私の名前はイヴァン・プジレフスキー、ヤンデックスで働いています。 過去6年間、データストレージとデータ処理のインフラストラクチャに携わってきましたが、今では、旅行、ホテル、チケットを求めて製品に切り替えました。 私はインフラストラクチャで長い間働いていたので、さまざまなロード済みアプリケーションを作成する方法について多くの経験を積んでいます。 当社のインフラストラクチャは、毎日24*7*365
、数千台のマシンで継続的に稼働しています。 当然、信頼性が高く効率的に機能し、会社が課すタスクを解決するようにコードを記述する必要があります。
今日は非同期についてお話します。 非同期とは何ですか? それは、何かと時間内の何かの不一致です。 この説明から、今日話すことは一般的に明確ではありません。 この問題をどういうわけか明確にするために、「Hello、world!」という例が必要です。 非同期性は通常、ネットワークアプリケーションを作成するコンテキストで発生するため、「Hello、world!」に類似したネットワークを作成します。 これはピンポンアプリです。 コードは次のようになります。
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
ソケットを作成し、そこから行を読み取り、pingであるかどうかを確認してから、応答としてpongを書き込みます。 非常にシンプルで明確。 コンピューター画面にこのようなコードが表示されるとどうなりますか? このコードは、これらの手順のシーケンスと考えています。
実際の物理的な時間の観点から、すべては少し偏っています。
そのようなコードを実際に書いて実行した人は、読み取りステップの後とステップの後
書き込みは、プログラムがコードの観点からは何もしていないように見えるかなり注目すべき時間間隔ですが、内部では「入出力」と呼ばれる機械が動作します。
I / Oの間、パケットはネットワークを介して交換され、すべての付随する低レベルの作業が行われます。 思考実験を行ってみましょう。このようなプログラムを1つ取得し、1つの物理プロセッサで実行し、オペレーティングシステムがないと想定して、どうなりますか? プロセッサは停止できず、命令に従わずにサイクルを続け、無駄なエネルギーを無駄にします。
この期間中に何か役に立つことができるかどうかという疑問が生じます。 私たちのアプリケーションは何もしていないように見えますが、非常に自然な質問です。これに対する答えは、プロセッサの電力を節約し、有用なものに使用することを可能にします。
3.基本コンセプト
3.1。 実行のスレッド
このタスクにどのようにアプローチできますか? 概念を調整しましょう。 基本的な操作またはステップの意味のあるシーケンスを参照して、「実行のフロー」と言います。 意味のあるものは、私が実行の流れについて話す文脈によって決まります。 つまり、シングルスレッドアルゴリズム(Aho-Korasik、グラフによる検索)について話している場合、このアルゴリズム自体は既に実行のスレッドです。 彼は問題を解決するためにいくつかのステップを踏みます。
私がデータベースについて話している場合、実行の1つのスレッドは、1つの着信要求を処理するためにデータベースによって実行されるアクションの一部である可能性があります。 Webサーバーについても同じことが言えます。 何らかのモバイルアプリケーションまたはWebアプリケーションを作成している場合、1つのユーザー操作(たとえば、ボタンのクリック、ネットワークの相互作用、ローカルストレージとの相互作用など)を提供します。 モバイルアプリケーションの観点から見たこれらのアクションのシーケンスは、実行の別の意味のあるフローにもなります。 オペレーティングシステムの観点から見ると、プロセスまたはプロセススレッドは意味のある実行スレッドでもあります。
3.2。 マルチタスクと同時実行
パフォーマンスの基礎は、そのようなトリックを実行する能力です。物理タイムベースにボイドを含む実行スレッドが1つある場合、これらのボイドを何か有用なもので埋めます-他の実行スレッドの手順に従います。
データベースは通常、同時に多くのクライアントにサービスを提供します。 より高いレベルの1つの実行スレッドのフレームワーク内で複数の実行スレッドの作業を組み合わせることができる場合、これはマルチタスクと呼ばれます。 つまり、マルチタスクとは、小さなタスクの解決に従属する1つの大きな実行フローのフレームワーク内でアクションを実行することです。
マルチタスクの概念と並列処理を混同しないようにすることが重要です。 並行性-
これらはランタイム環境のプロパティであり、1つのステップで、1つのステップで、異なる実行スレッドで進行することができます。 2つの物理プロセッサがある場合、1クロックサイクルで2つの命令を実行できます。 プログラムが1つのプロセッサで実行されている場合、同じ2つの命令を実行するには2クロックサイクルかかります。
これらの概念は異なるカテゴリに分類されるため、混同しないようにすることが重要です。 マルチタスクは、プログラムの機能であり、さまざまなタスクの可変作業として内部的に構成されます。 並行性はランタイム環境のプロパティであり、1クロックサイクルで複数のタスクを処理できます。
多くの点で、非同期コードと非同期コードの作成はマルチタスクコードの作成です。 主な困難は、タスクのエンコード方法とそれらの管理方法です。 したがって、今日はこれについて話します-マルチタスクコードの記述。
4.ブロックと待機
いくつかの簡単な例から始めましょう。 ピンポンに戻る:
socket s; string x; x = read_from_socket(s, 4); if (x == "ping") { write_to_socket(s, "pong"); } return;
すでに説明したように、読み取りと白線が実行された後、実行スレッドはスリープ状態になり、ブロックされます。 通常、「フローはブロックされます」と言います。
socket s; string x; x = read_from_socket(s, 4); /* thread is blocked here */ if (x == "ping") { write_to_socket(s, "pong"); /* thread is blocked here */ } return;
これは、実行のフローが、イベントを継続するためにイベントが必要になるポイントに到達したことを意味します。 特に、ネットワークアプリケーションの場合、データがネットワークを介して到着する必要があります。逆に、データをネットワークに書き込むためのバッファを解放しました。 イベントは異なる場合があります。 時間の側面について話している場合は、タイマーが起動するか、別のプロセスが完了するのを待つことができます。 ここでのイベントは一種の抽象的なものであり、それらについて期待できることを理解することが重要です。
単純なコードを記述するとき、イベントの期待値の制御をより高いレベルに暗黙的に与えます。 私たちの場合、オペレーティングシステム。 彼女は、より高いレベルのエンティティとして、次に実行するタスクを選択する責任があり、イベントの発生を追跡する責任もあります。
開発者として記述するコードは、1つのタスクの作業に関して同時に構造化されます。 例のコードスニペットは1つの接続を処理します。1つの接続からpingを読み取り、1つの接続にpongを書き込みます。
コードは明確です。 それを読んで、それが何をするのか、どのように機能するのか、どの問題を解決するのか、どんな不変条件を持っているのかなどを理解できます。 同時に、このようなモデルでは、タスクプランニングの管理が非常に不十分です。 一般に、オペレーティングシステムには優先順位の概念がありますが、ソフトリアルタイムシステムを記述した場合、Linuxで利用可能なツールでは十分な健全なリアルタイムシステムを作成するには不十分であることがわかります。
さらに、オペレーティングシステムは複雑なものであり、アプリケーションからカーネルへのコンテキストの切り替えには数マイクロ秒かかります。これは、いくつかの簡単な計算で、1秒あたり約20〜10万のコンテキストスイッチの推定値になります。 つまり、Webサーバーを記述する場合、リクエストの処理はシステムの10倍の費用がかかると仮定して、1秒で約2万件のリクエストを処理できます。
4.1。 ノンブロッキング待機
ネットワークをより効率的に使用する必要がある場合は、インターネットでヘルプを探し始め、select / epollを使用します。 インターネットでは、数千の接続を同時に提供する場合、epollが必要であると書かれています。これは優れたメカニズムなどであるためです。 ドキュメントを開くと、次のようなものが表示されます。
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); void FD_CLR(int fd, fd_set* set); int FD_ISSET(int fd, fd_set* set); void FD_SET(int fd, fd_set* set); void FD_ZERO(fd_set* set); int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
インターフェースに使用する記述子(selectの場合)または通過するイベントの多くが含まれる関数
アプリケーションの境界を越えて、処理する必要があるオペレーティングシステムのカーネル(epollの場合)。
また、select / epollではなく、libuvなどのライブラリにアクセスできることも追加する価値があります。libuvにはAPIにイベントはありませんが、多くのコールバックがあります。 ライブラリインターフェイスは、「親愛なる友よ、ソケットを読み取るためのコールバックを提供します。これは、データが表示されたときに呼び出します。」
int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat); typedef void (*uv_timer_cb)(uv_timer_t* handle); int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb); int uv_read_stop(uv_stream_t*); typedef void (*uv_read_cb)(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf); int uv_write(uv_write_t* req, uv_stream_t* handle, const uv_buf_t bufs[], unsigned int nbufs, uv_write_cb cb); typedef void (*uv_write_cb)(uv_write_t* req, int status);
前章の同期コードと比べて何が変わったのですか? コードは非同期になりました。 これは、イベントを監視する時点を決定するためにアプリケーションにロジックを取り入れたことを意味します。 select / epollへの明示的な呼び出しは、発生したイベントに関する情報をオペレーティングシステムに要求するポイントです。 また、次に実行するタスクの選択をアプリケーションコードに取り入れました。
インターフェイスの例から、マルチタスクを導入するための基本的に2つのメカニズムがあることがわかります。 ある種の「プル」は
待っているイベントの多くを引き出して、何らかの形でそれらに反応します。 このアプローチでは、オーバーヘッドを1つで簡単に償却できます
イベントであるため、発生したイベントのセットに関する通信で高いスループットを達成します。 通常、カーネルとネットワークカードの相互作用や、ユーザーとオペレーティングシステムの相互作用など、すべてのネットワーク要素は、ポーリングメカニズムに基づいて構築されます。
2番目の方法は、「プッシュ」メカニズムです。特定の外部エンティティが明らかに入ってくると、実行のフローを中断し、「今、到着したイベントを処理してください」と言います。 これは、外部エンティティが実行スレッドに明らかに侵入し、「今、このイベントに取り組んでいます。」と言うとき、コールバック、Unixシグナル、プロセッサレベルでの割り込みを使用したアプローチです。 このアプローチは、イベントの発生とそれに対する反応の間の遅延を減らすために登場しました。
特定のアプリケーションの問題を記述して解決するC ++開発者が、イベントモデルをコードにドラッグする必要があるのはなぜですか? 多くのタスクの作業をコードにドラッグアンドドロップして管理すると、カーネルへの移行がないため、またその逆の場合、単位時間あたりの作業が少し速くなり、より便利なアクションを実行できます。
これは、私たちが書いたコードに関して何につながりますか? たとえば、非常に一般的な高性能HTTPサーバーであるnginxを取り上げます。 コードを読むと、非同期モデルに基づいて構築されています。 コードは読みにくいです。 単一のHTTPリクエストを処理するときに正確に何が起こるかを自問すると、コードベースのさまざまな角度で、さまざまなファイルに区切られたコード内のフラグメントがたくさんあることがわかります。 各フラグメントは、HTTPリクエスト全体の処理の一部として少量の作業を行います。 例:
static void ngx_http_request_handler(ngx_event_t *ev) { … if (c->close) { ngx_http_terminate_request(r, 0); return; } if (ev->write) { r->write_event_handler(r); } else { r->read_event_handler(r); } ... } /* where the handler... */ typedef void (*ngx_http_event_handler_pt)(ngx_http_request_t *r); struct ngx_http_request_s { /*... */ ngx_http_event_handler_pt read_event_handler; /* ... */ }; /* ...is set when switching to the next processing stage */ r->read_event_handler = ngx_http_request_empty_handler; r->read_event_handler = ngx_http_block_reading; r->read_event_handler = ngx_http_test_reading; r->read_event_handler = ngx_http_discarded_request_body_handler; r->read_event_handler = ngx_http_read_client_request_body_handler; r->read_event_handler = ngx_http_upstream_rd_check_broken_connection; r->read_event_handler = ngx_http_upstream_read_request_handler;
要求構造があります。これは、ソケットが読み取りまたは書き込みアクセスを通知するときにイベントハンドラーに転送されます。 さらに、このハンドラーは、要求処理の状態に応じて、プログラムの過程で常に切り替わります。 ヘッダーを読み取るか、リクエストの本文を読み取るか、アップストリームにデータを要求します。一般的に、さまざまな状態があります。
そのようなコードは、本質的に、イベントに対する反応の観点から記述されているため、読みにくいです。 私たちはそのような状態にあり、来た出来事に特定の方法で反応します。 HTTP要求を処理するプロセス全体の全体像はありません。
JavaScriptでよく使用されるもう1つのオプションは、コールバックをインターフェイスコールに転送するときにコールバックベースのコードを構築することです。通常、イベントには他のネストされたコールバックなどがあります。
int LibuvStreamWrap::ReadStart() { return uv_read_start(stream(), [](uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(handle->data)->OnUvAlloc(suggested_size, buf); }, [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) { static_cast<LibuvStreamWrap*>(stream->data)->OnUvRead(nread, buf); }); } /* ...for example, parsing http... */ for (p=data; p != data + len; p++) { ch = *p; reexecute: switch (CURRENT_STATE()) { case s_start_req_or_res: /*... */ case s_res_or_resp_H: /*... */ case s_res_HT: /*... */ case s_res_HTT: /* ... */ case s_res_HTTP: /* ... */ case s_res_http_major: /*... */ case s_res_http_dot: /*... */ /* ... */
ここでもコードは非常に断片化されており、リクエストに対する現在の状態を理解することはできません。 多くの情報がクロージャを介して送信され、単一のリクエストを処理するロジックを再構築するために精神的な努力が必要です。
したがって、マルチタスク処理をコードに導入すると(作業タスクを選択して多重化するロジック)、効果的なコードを取得し、タスクの優先順位付けを制御できますが、読みやすさの点で多くが失われます。 このコードは読みにくく、保守が困難です。
なんで? たとえば、ファイルを読み取ってネットワーク経由で転送するなど、単純なケースがあるとします。 非ブロッキングバージョンでは、このケースはそのような線形状態マシンに対応します。
- 初期状態
- ファイルの読み取りを開始し、
- ファイルシステムからの応答を待って、
- ファイルをソケットに書き込む、
- 最終状態。
ここで、データベースからこのファイルに情報を追加するとします。 簡単なオプション:
- 初期状態
- ファイルを読む
- ファイルを読む
- データベースから読み取ります
- データベースから読み取る
- ソケットでの作業
- ソケットに書き込みました。
線形コードのように見えますが、状態の数が増えています。
次に、ファイルとデータベースからの読み取りという2つのステップを並列化するとよいと考え始めます。 組み合わせ論の奇跡が始まります。あなたは初期状態にあり、データベースからファイルとデータを読み取ることを要求しています。 次に、データベースからのデータはあるが、ファイルがない状態、またはその逆の状態になります。ファイルからはデータがあり、データベースからはデータがありません。 次に、次の2つのいずれかの状態になる必要があります。 繰り返しますが、これらは2つの状態です。 次に、両方の成分が含まれている状態にする必要があります。 次に、それらをソケットなどに書き込みます。
アプリケーションが複雑になるほど、状態が増えるほど、頭の中で組み合わせる必要のあるコードフラグメントが増えます。 不便。 または、コールバックヌードルを書いているので、読むのが不便です。 分岐システムが作成されている場合、ある日、それを許容できなくなる時期が来ます。
5.先物/約束
問題を解決するには、状況を簡単に調べる必要があります。
プログラムがあり、黒と赤の円があります。 実行の流れは黒丸です。 ストリームが作業を続行できない場合、赤で交互に表示されることがあります。 問題は、実行の黒いスレッドのために、次の黒い円に入る必要があることです。
問題は、プログラミング言語でコードを書くとき、今何をすべきかをコンピューターに説明することです。 コンピューターは比較的単純なもので、プログラミング言語で記述された命令を必要とします。 彼女は次のサークルの指示を待っています。私たちのプログラミング言語では、「将来、何かが起こったら、何かをしてください」と言うだけのお金がありません。
プログラミング言語では、関数の呼び出し、算術演算など、理解できる瞬間的なアクションで動作します。 次の特定の次のステップについて説明します。 同時に、アプリケーションロジックを処理するには、次の物理的なステップではなく、次の論理的なステップを記述する必要があります。たとえば、データベースのデータが表示された場合の対処方法です。
したがって、これらのフラグメントを結合するためのメカニズムが必要です。 同期コードを作成した場合、この質問を完全に覆い隠し、オペレーティングシステムがそれを処理し、スレッドの中断と再スケジュールを許可すると述べました。
レベル1では、このPandoraのボックスを開き、コードに多くの切り替え、ケース、条件、ブランチ、状態を追加しました。 コードが比較的読みやすいが、レベル1のすべての利点を保持するために、何らかの妥協が必要です。
幸いなことに、1988年に分散システムに携わる人々であるBarbara LiskovとLyub Shirirが問題を認識し、言語の変更が必要になりました。 プログラミング言語に構造を追加して、イベント間の時間的関係を表現する必要があります-現在の瞬間と将来の不確実な瞬間。
これらは約束と呼ばれます。 コンセプトはクールですが、20年もの間棚にほこりを集めてきました。 — , Twitter, Ruby on Rails Scala, , , , future . Your Server as a Function. , .
Scala, , ++ ?
, Future. T c : , - .
template <class T> class Future <T>
, , , . , «», , . Future «», Promise — «». ; , JavaScript, Promise — , Java – Future.
, . , , boost::future ( std::future) — , .
5.1。 Future & Promise
template <class T> class Future { bool IsSet() const; const T& Get() const; T* TryGet() const; void Subscribe(std::function<void(const T&)> cb); template <class R> Future<R> Then( std::function<R(const T&)> f); template <class R> Future<R> Then( std::function<Future<R>(const T&)> f); }; template <class T> Future<T> MakeFuture(const T& value);
, , - , . , , , . , , — , , . Then, .
template <class T> class Promise { bool IsSet() const; void Set(const T& value); bool TrySet(const T& value); Future<T> ToFuture() const; }; template <class T> Promise<T> NewPromise();
. , . «, , , ».
5.2。
? , . Then — , .
, — future --, - t — . , , , f, - r.
t f. , , r.
: t, , r . :
template <class T> template <class R> Future<R> Future<T>::Then(std::function<R(const T&)> f) { auto promise = NewPromise<R>(); this->Subscribe([promise] (const T& t) { auto r = f(t); promise.Set(r); }); return promise.ToFuture(); }
:
-
Promise
R
, -
Future<T>
t
, - ,
r = f(t)
, -
r
Promise
, -
Promise
.
f
, R
, Future<R>
, R
. :
-
T
, - ,
T
, ,R
, - ,
R
, , .
template <class T> template <class R> Future<R> Future<T>::Then(std::function<Future<R>(const T&)> f) { auto promise = NewPromise<R>(); this->Subscribe([promise] (const T& t) { auto that = f(t); that.Subscribe([promise] (R r) { promise.Set(r); }); }); return promise.ToFuture(); }
, - t. f, r, . , , .
, Then :
-
Promise
, -
Subscribe
-, -
Promise
,Future
.
, . , , , .
, , , -. , , -, Subscribe. , , , - . , .
5.3。 例
AsyncComputeValue, GPU, . Then, , (2v+1) 2 .
Future<int> value = AsyncComputeValue(); // value.Subscribe([] (int v) { std::cerr << "Value is: " << v << std::endl; });
. , : (2v+1) 2 . , .
// (2v+1)^2 Future<int> anotherValue = value .Then([] (int v) { return 2 * v; }) .Then([] (int u) { return u + 1; }) .Then([] (int w) { return w * w; });
, , . .
2番目の例。 : , ; ; .
Future<int> GetDbKey(); Future<string> LoadDbValue(int key); Future<void> SendToMars(string message); Future<void> ExploreOuterSpace() { return GetDbKey() // Future<int> .Then(&LoadDbValue) // Future<string> .Then(&SendToMars); // Future<void> } ExploreOuterSpace().Subscribe( [] () { std::cout << "Mission Complete!" << std::endl; });
— ExploreOuterSpace. Then; — — , . ( ) . .
5.4。 Any-
: Future
, , . , , :
template <class T> Future<T> Any(Future<T> f1, Future<T> f2) { auto promise = NewPromise<T>(); f1.Subscribe([promise] (const T& t) { promise.TrySet(t); }); f2.Subscribe([promise] (const T& t) { promise.TrySet(t); }); return promise.ToFuture(); } //
, Any-, Future : , . , , .
, , , , , . « DB1, DB2, — - ».
5.5。 All-
. , , , ( T1 T2), T1 T2 , , .
template <class T1, class T2> Future<std::tuple<T1, T2>> All(Future<T1> f1, Future<T2> f2) { auto promise = NewPromise<std::tuple<T1, T2>>(); auto result = std::make_shared< std::tuple<T1, T2> >(); auto counter = std::make_shared< std::atomic<int> >(2); f1.Subscribe([promise, result, counter] (const T1& t1) { std::get<0>(*result) = t1; if (--(*counter) == 0) { promise.Set(*result)); } }); f2.Subscribe([promise, result, counter] (const T2& t2) { /* */ } return promise.ToFuture(); } //
nginx. , , . nginx « », « », « » . All- , . .
5.6.
Future Promises — legacy-, . callback- , , : Future, , callback- Future.
// cb void LegacyAsyncComputeStuff(std::function<void(int)> cb); // Future Future<int> ModernAsyncComputeStuff() { auto promise = NewPromise<int>(); LegacyAsyncComputeStuff( [promise] (int value) { promise.Set(value); }); return promise.ToFuture(); }
: , Future .
6.
, , . .
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); // req 2 : QueryBackend Reply GetRequest().Subscribe( [] (Request req) { auto rsp = QueryBackend(req) .Then(&HandlePayload) .Then(Bind(&Reply, req)); });
. Request, - . , . , , , . , - .
, , . どうする — , request payload, — , .
, Java Netty. , , . , , .
, GetRequest, QueryBackend, HandlePayload Reply , Future.
, , Future T — WaitFor.
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future); // req 2 : QueryBackend Reply GetRequest().Subscribe( [] (Request req) { auto rsp = QueryBackend(req) .Then(&HandlePayload) .Then(Bind(&Reply, req)); });
:
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future); auto req = WaitFor(GetRequest()); auto pld = WaitFor(QueryBackend(req)); auto rsp = WaitFor(HandlePayload(pld)); WaitFor(Reply(req, rsp));
: Future, . . , . .
. . - 0, , , mutex+cvar future. . , .
6.1。 コルーチン
, . , , , , - , . , - .
— «» , , . . . : boost::asio boost::fiber.
, . どうやってやるの?
6.2。 WaitFor
, , boost::context, : , ; , . x86/64 , , .
// class MachineContext; // from, to void SwitchContext(MachineContext* from, MachineContext* to); // – boost::context // // * x86_64-ASM (push...-movq(rsp,eip)-pop...-jmpq) // * makecontext/swapcontext // * setjmp/longjmp
, goto: , , , .
, - . Fiber — . +Future. , , Future, .
class Fiber { /* */ MachineContext context_; Future<void> future_; };
class Scheduler { /* */ void WaitFor(Future<void> future); void Loop(); MachineContext loop_context_; Fiber* current_fiber_; std::deque<Fiber*> run_queue_; };
Future , , , . : Loop, , , , , .
WaitFor?
thread_local Scheduler* ThisScheduler; template <class T> T WaitFor(Future<T> future) { ThisScheduler->WaitFor(future.As<void>()); return future.Get(); } void Scheduler::WaitFor(Future<void> future) { current_fiber_->future_ = future; SwitchContext(¤t_fiber_->context_, &loop_context_); }
: , - , , Future void, . .
Future<void>
, , - .
WaitFor : : « Fiber Future», ( ) .
, :
ThisScheduler->WaitFor
return future.Get()
, .
? , Future, .
6.3。
- , , , - , . SwitchContext , 2 — .
void Scheduler::Loop() { while (true) { // (1) (= !) current_fiber_ = run_queue_.front(); run_queue_.pop_front(); SwitchContext(&loop_context_, ¤t_fiber_->context_); // (2) , //…
次に何が起こりますか? , , , Future, Future, , , .
void Scheduler::Loop() { while (true) { // (1) … // (2) , if (current_fiber_->future_) { current_fiber_->future_.Subscribe( [this, fiber = current_fiber_] { fiber->future_ = nullptr; run_queue_.push_back(fiber); }); } //…
, . :
WaitFor — .
Switch- .
Future ( ), , . - Fiber.
WaitFor Future , - , Future . :
Future<Request> GetRequest(); Future<Payload> QueryBackend(Request req); Future<Response> HandlePayload(Payload pld); Future<void> Reply(Request req, Response rsp); template <class T> T WaitFor(Future<T> future); auto req = WaitFor( GetRequest()); auto pld = WaitFor( QueryBackend(req)); auto rsp = WaitFor( HandlePayload(pld)); WaitFor( Reply(req, rsp));
, , , . , , .
6.4. Coroutine TS
? — . Coroutine TS, , WaitFor CoroutineWait, CoroutineTS — - . , - . , Waiter Co, , .
7. ?
. , , , . , , , .
— . , . . , . , , , , .
- , , . , . , , .
, ? , .
. , , , , . . , , , , .
nginx, , , , , . , , , future promises.
, , , , , , , .
futures, promises actors. . , .
: , , , . , , , , . ? , .
広告の分。 19-20 C++ Russia 2019. , , Grimm Rainer «Concurrency and parallelism in C++17 and C++20/23» , C++ . , . , , - .