この記事は機械翻訳されたものです。

Windows と C++

再開可能な関数で未来に立ち戻る (機会翻訳)

Kenny Kerr

 

Kenny Kerr私の最後の列を締結 (msdn.microsoft.com/magazine/jj618294) C いくつかの可能な改善を強調することによって + + 11 の先物とそれらを主に学術、実用的で有用な効率的かつ構成可能な非同期システム構築のために単純化されてから変換と約束。 大部分は、このニクラス ・ グスタフソンと Artur Laksberg の Visual C チームからの仕事に触発されました。

先物の将来の表現としては、これらの線に沿って例を与えた:

 

int main()
{
  uint8 b[1024];
  auto f = storage_read(b, sizeof(b), 0).then([&]()
  {
    return storage_write(b, sizeof(b), 1024);
  });
  f.wait();
}

Storage_read と storage_write の両方の機能は将来のある時点で、将来完了可能性がありますそれぞれの I/O 操作を表すを返します。 これらの関数は、いくつかのストレージ ・ サブシステム 1 KB ページを持つモデルします。 プログラム全体は、最初のページからストレージ バッファーに読み取り、それはストレージの 2 番目のページに戻るコピーします。 この例の目新しさには、シームレスに待んだができます、単一の論理 I/O 操作で構成される読み取りおよび書き込み操作を許可する将来のクラスに追加仮説「、」メソッドの使用されています。

これは私が私の最後のコラムで説明巨大な改善でスタックをリッピングの世界まだ自体にはまだかなり私私 2012 年 8 月のコラムでは、"軽量協同組合マルチタスクと C++"説明言語によってサポートされてコルーチンのような施設のユートピア的な夢です (msdn.microsoft.com/magazine/jj553509)。 その列に私は正常にいくつかの劇的なマクロの策略とそのような施設を実現する方法を実証 — が、主にローカル変数を使用不能に関連しない重大な欠点なし。 今月のこれは、C++ 言語で達成する可能性がありますいくつかの考えを共有したいと思います。

私は必ずしも現実は今日の仕事のソリューションを必要があるために同時実行の制御には実用的なソリューションを達成するための代替技術を探る記事のこのシリーズを始めた。 私たちは、ただし、未来を見るし、C++ をプッシュするコミュニティより自然で生産的な方法で I/O 集中アプリケーションを作成するための大きいサポートを要求することによって進む必要があります。 確かに書き込み非常にスケーラブルなシステム JavaScript および c# のプログラマと十分な意志の力とまれな C++ プログラマの排他的な範囲はいけません。 また、これだけでは利便性とエレガンスは、プログラミングの構文とスタイルでないことに注意してください。 任意の時点でアクティブな複数の I/O 要求を持っている能力は、パフォーマンスを劇的に向上させる可能性があります。 ストレージおよびネットワーク ドライバーがうまく多くの I/O 要求のフライトとスケールに設計されています。 ストレージ ドライバーの場合は、要求はハードウェア バッファリング、削減を組み合わせることができますシーク時間。 ネットワーク ドライバーの場合より多くの要求はより大きなネットワーク パケットや最適化のスライディング ウィンドウ操作を意味します。

私はスイッチするつもり複雑さが頭をもたげてどのように迅速に少し説明する歯車。 方法について単に読み書きしてではなくストレージ デバイスから、ファイルの内容をネットワーク接続を介して提供して? 前に、同期のアプローチで開始し、そこから作業つもり。 コンピューターは基本的に非同期かもしれないが、我々 単なる人間は確かではありません。 私はあなたについて知らないが、同時にサイトの多くを行ったことがないです。 次のクラスについて考えてみましょう。

class file { uint32 read(void * b, uint32 s); };
class net { void write(void * b, uint32 s); };

あなたの想像力は残りの部分を埋めるために使用します。 いくつかのファイルから読み取るバイト数ができますファイルのクラスだけです。 私はさらに、ファイル オブジェクトのオフセットについて追跡されますと仮定します。 同様に、net クラス データ オフセットが必ずしも呼び出し元から隠されているそのスライディング ウィンドウ方式の実装を介して TCP によって処理される TCP ストリームをモデルがあります。 さまざまな理由から、おそらくキャッシュまたは競合に関連するファイルを読み取るメソッドは常に実際に要求されたバイト数を返すことはありません。 ファイルの末尾に達したときただし、のみ 0 返されます。 ネットの書き込みメソッドは、TCP の実装は、仕様では、ありがたいことにこれを呼び出し元にはシンプルに作業の膨大な量はので簡単です。 これがかなり代表的な OS の I/O の基本の架空のシナリオです。 私は今すぐ次の簡単なプログラムを書くことができます。

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  while (auto actual = f.read(b, sizeof(b)))
  {
    n.write(b, actual);
  }
}

10 KB のファイルを考えると、次の一連のイベント ループが尽きる前に想像してください。

read 4096 bytes -> write 4096 bytes ->
read 4096 bytes -> write 4096 bytes ->
read 2048 bytes -> write 2048 bytes ->
read 0 bytes

私の最後の列で、同期の例のようそれ何がここでは、C++ のシーケンシャルの性質のおかげで起こっているを把握するハードではありません。 非同期のコンポジションにスイッチを作ることは少し困難です。 最初のステップは、先物を返す net のクラス ファイルを変換します。

class file { future<uint32> read(void * b, uint32 s); };
class net { future<void> write(void * b, uint32 s); };

それは簡単だった。 これらのメソッドに任意の非同期性を利用する主な機能を書き換え、いくつかの課題を提示します。 私はもはや単にシーケンシャル構成を扱っているためそれはもはや未来の仮想的な「、」メソッドを使用するには十分です。 はい、読み取りが実際に何かを読み取る場合、書き込みだけを読むに従っている true です。 問題を複雑にさらに、読み取りまた書き込みのすべてのケースでします。 あなたが閉鎖の面と思うに誘惑されるかもしれないが状態と行動の組成と動作とその他の動作の組成ではないという概念をカバーします。

[読み取りのみクロージャを作成して起動でき書き込み操作。

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](uint32 actual) { n.write(b, actual); };

将来の方法を知っていないので、何の書き込み関数に渡す、もちろん、これはまったく機能しません。

read().then(write);

これに対処するには、何らかの状態を転送するには、先物取引を許可する規則を必要とします。 当然の選択は、(おそらく) 未来そのものを転送するためです。 [メソッド、将来私がこれを書くことができる、適切な型のパラメーターを取る式を期待します。

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](future<uint32> previous) { n.write(b, 
  previous.get()); };
read().then(write);

これは、作品と構成可能性をさらに次のメソッドで予測式は未来も戻ることを定義することによって向上したい可能性がありますも。 ただし、条件付きループを表現する方法の問題は残ります。 最終的には、それこれは、反復的な方法で表現する方が簡単ですので、元のループ、do...while ループとして代わりに、再考するより簡単であると証明します。 私は先物を条件付きでチェーン、<bool>、将来の結果に基づいて最後に反復の組成をもたらすこのパターンは、非同期方式で模倣する do_while アルゴリズムを考案するし 値の例:

future<void> do_while(function<future<bool>()> body)
{
  auto done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();  
}

Do_while 関数は、まずループの終了の信号をその究極の将来の参照カウントの約束を作成します。 これはループの本体を表す関数と共に反復関数に渡されます。

void iteration(function<future<bool>()> body, 
  shared_ptr<promise<void>> done)
{
  body().then([=](future<bool> previous)
  {
    if (previous.get()) { iteration(body, done); }
    else { done->set_value(); }
  });
}

この反復関数から 1 つの呼び出しチェーンは次としてブレーク アウトと完了を通知する機能を提供する、do_while のアルゴリズムの中心です。 再帰的に見えるかもしれないが、全体のポイントのスタックからの非同期操作を分離することで、したがって、ループは実際にスタックを成長していない覚えています。 Do_while アルゴリズムを使用しては比較的簡単で、私は今で示されているプログラムを書くことができます図 1

図 1 do_while アルゴリズムを使用して

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  auto loop = do_while([&]()
  {
    return f.read(b, sizeof(b)).then([&](future<uint32> previous)
    {
      return n.write(b, previous.get());
    }).then([&]()
    {
      promise<bool> p;
      p.set_value(!f.eof);
      return p.get_future();
    });
  });
  loop.wait();
}

Do_while 関数は、当然のことながら、未来を返しますとこの場合は、待つが、これは簡単に shared_ptrs をヒープ上のメイン関数のローカル変数を格納することによって回避されていること。 Do_while 関数に渡されるラムダ式の内部に、読み取り操作での書き込み操作の後に開始します。 この例をシンプルに保つには、その書き込みはすぐに戻りゼロ バイトを書くことに言ったしているかどうかと仮定します。 書き込み操作が完了したら、ファイルのファイル終了ステータスをチェックし、ループの条件の値を提供する将来戻る。 これは、ファイルのコンテンツがなくなるまでループの本体を繰り返すことができます。

このコードは、特に不愉快ではないが- そして、確かに、スタックをリッピングよりも間違いなく多くの洗剤である — 少しのサポート言語から長い道のりを行くでしょう。 ニクラス ・ グスタフソンがすでにこのようなデザインを提案し、「再開可能関数」と呼ばれる先物約束の提案する改善の建物と少し構文糖を追加、次のように驚くほど複雑な非同期操作をカプセル化する再開関数を記述することができます。

future<void> file_to_net(shared_ptr<file> f, 
  shared_ptr<net> n) resumable
{
  uint8 b[4096];
  while (auto actual = await f->read(b, sizeof(b)))
  {
    await n->write(b, actual);
  }
}

このデザインの美しさは、コードを元の同期バージョンは驚くほどよく似ているし、は何、すべての後に探しています。 次の関数のパラメーター リスト「再開」コンテキスト キーワードに注意してください。 これは、私は私の 2012 年 8 月のコラムで説明した仮想的な"async"キーワードに似ています。 その列の説明とは異なり、しかし、これはコンパイラそのものによって実装されます。 したがってがない合併症や制限などのマクロ実装に直面しています。 Switch ステートメントとローカル変数を使用することができる — とコンス トラクターとデストラクターは期待どおりに動作する — があなたの機能は、今一時停止し、再開私のマクロを試作する同様の方法でことができるでしょう。 それだけでなく、しかし、あなただけそれらがラムダ式を使用する場合のよくある間違いのスコープ外にいるため、ローカル変数をキャプチャするの落とし穴から解放されるでしょう。 コンパイラ ヒープ上の再開関数内部のローカル変数のストレージを提供することの世話をします。

前述の例でも、読み取りの前の「待つ」キーワードに気づくし、メソッド呼び出しを記述します。 このキーワード再開ポイントを定義し、それを一時停止し、後で再開するかどうか、または非同期操作が同期的に完了するが起こった場合に実行を続行するかどうかを判断することができる未来のようなオブジェクトの結果は、式を見込みます。 明らかに、最高のパフォーマンスを達成するには、私はあまりにも共通のシナリオの非同期操作の完了を同期的に、おそらくキャッシュが原因または高速失敗シナリオを処理する必要があります。

私が await キーワードは将来のようなオブジェクトを見込んでいるということに注意してください。 厳密に言えば、それは、実際の将来のオブジェクトする必要があります理由はありません。 それだけの非同期完了とシグナルの検出をサポートするために必要な動作を提供する必要があります。 これは、テンプレート今日は仕事の方法に似ています。 この未来のようなオブジェクトは、既存の get メソッドだけでなく私は前回のコラムで説明し、メソッドをサポートする必要があります。 結果がすぐに利用可能ですのケースでパフォーマンスを向上させるため、提案 try_get および is_done メソッドも有用で 。 もちろん、コンパイラの最適化をすることができますこのような方法の可用性に基づいて。

これは、それが見えるかもしれないほどあり得ない話ではないです。 C# すでにほぼ同一の施設再開機能の道徳的な等量の非同期メソッドの形でいます。 それもしたように同じ方法で動作する await キーワードを提供します。 我々 すべての自然と簡単に効率的かつ構成可能な非同期システムを書くことができるでしょうので、私の希望は、C++ コミュニティ再開関数、または何かのように、受け入れることができます。

再開機能の詳細な分析は、彼らが実装する可能性がある方法を見てを含む読んでくださいニクラス ・ グスタフソン紙、「再開関数で bit.ly/zvPr0a.

Kenny Kerr ソフトウェア職人のネイティブ Windows 開発への情熱です。 彼に到達 kennykerr.ca

この記事のレビュー、次技術専門家のおかげで:Artur Laksberg