web-dev-qa-db-ja.com

async-awaitが追加のスレッドを作成しない場合、アプリケーションはどのように応答するのでしょうか。

繰り返しますが、async-awaitを使用しても追加のスレッドは作成されないことがわかりました。コンピュータが一度に複数のことを実行しているように見えることができる唯一の方法があるため、これは意味がありません。

  • 実際には一度に複数のことを実行します(並列実行、複数のプロセッサの使用)
  • タスクをスケジュールし、それらを切り替えることでそれをシミュレートします(少しのA、少しのB、少しのAなどを実行します)。

それでasync-awaitがそれらのどちらでもない場合、どうすればアプリケーションをレスポンシブにすることができるでしょうか。スレッドが1つしかない場合、任意のメソッドを呼び出すことは、他の処理を行う前にそのメソッドが完了するのを待つことを意味し、そのメソッド内のメソッドは処理を進める前に結果を待つ必要があります。

198
Ms. Corlib

実際、async/awaitはそれほど魔法ではありません。トピック全体は非常に広範ですが、あなたの質問に対する迅速かつ完全な回答を得るには、管理できると思います。

Windowsフォームアプリケーションで単純なボタンクリックイベントに取り組みましょう。

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

explicitlynotGetSomethingAsyncが現在戻ってきているものについて話します。これは、たとえば2秒後に完了するものだとしましょう。

従来の非同期ではない世界では、ボタンクリックイベントハンドラは次のようになります。

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

フォームのボタンをクリックすると、アプリケーションが約2秒間フリーズしたように見えますが、このメソッドが完了するまで待機します。起こるのは、基本的にループである「メッセージポンプ」がブロックされることです。

このループは、「マウスを動かしたり、クリックしたりするなど、何かしたことがありますか?何かを塗り直す必要がありますか?そして、その「何か」を処理します。このループは、ユーザーが "button1"(またはWindowsからの同等のメッセージ)をクリックしたというメッセージを受け取り、上記のbutton1_Clickメソッドを呼び出してしまいました。このメソッドが戻るまで、このループは待機状態のままになります。これには2秒かかり、この間、メッセージは処理されません。

ウィンドウを扱うほとんどのことはメッセージを使用して行われます。つまり、メッセージループがほんの一瞬でもメッセージのポンピングを停止すると、ユーザーはすぐに気づきます。たとえば、自分のプログラムの上にメモ帳または他のプログラムを移動してから再び離れると、ウィンドウのどの領域が突然再び表示されたかを示すペイントメッセージがプログラムに送信されます。これらのメッセージを処理するメッセージループが何かを待っているか、ブロックされている場合、ペイントは行われません。

したがって、最初の例でasync/awaitが新しいスレッドを作成しない場合、どのようにそれを行いますか?

さて、あなたのメソッドは2つに分割されます。これは、これらの広範なトピックタイプの1つであるため、あまり詳細に説明しませんが、このメソッドは次の2つに分割されていると言えば十分です。

  1. awaitへの呼び出しを含む、GetSomethingAsyncまでのすべてのコード
  2. awaitに続くすべてのコード

図:

code... code... code... await X(); ... code... code... code...

再配置:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

基本的に、メソッドは次のように実行されます。

  1. awaitまでのすべてを実行します
  2. GetSomethingAsyncメソッドを呼び出し、その処理を行い、2秒後に完了するものを返します。

    これまでのところ、メインスレッドで発生し、メッセージループから呼び出されるbutton1_Clickの元の呼び出しの内部にいます。 awaitまでのコードに時間がかかる場合でも、UIはフリーズします。この例では、それほど多くありません

  3. awaitキーワードは、いくつかの巧妙なコンパイラーマジックと一緒に、基本的に「OK 、私たちが待ち望んでいること)完了に取り掛かります。まだ実行するコードが残っているので教えてください」。

    実際には SynchronizationContextクラス が完了したことを通知します。これは、現在プレイ中の実際の同期コンテキストに応じて、実行のためにキューに入れられます。 Windows Formsプログラムで使用されるコンテキストクラスは、メッセージループがポンピングしているキューを使用してキューに入れます。

  4. そのため、メッセージループに戻り、ウィンドウの移動、サイズ変更、または他のボタンのクリックなど、メッセージを自由に送り続けることができます。

    ユーザーにとって、UIは再び反応するようになり、他のボタンクリックの処理、サイズ変更、そして最も重要なこととして、redrawingなので、凍結しているように見える。

  5. 2秒後、私たちが待っているのは完了し、今何が起こるかということです(同期コンテキスト)は、メッセージループが見ているキューにメッセージを置きます。そして、このコードはすべてafter the waitです。
  6. メッセージループがそのメッセージに到達すると、awaitの直後に、中断したところからメソッドを基本的に「再入力」し、残りのメソッドの実行を継続します。このコードはメッセージループから再度呼び出されるため、このコードがasync/awaitを適切に使用せずに長い時間を実行すると、メッセージループが再びブロックされることに注意してください。

ボンネットの下には多くの可動部分があるので、ここに詳細情報へのリンクがあります。「必要」と言うつもりでしたが、このトピックisは非常に広く、知ることがかなり重要ですこれらの可動部の一部。常に、async/awaitは依然として漏れやすい概念であることを理解するでしょう。根本的な制限と問題の一部は、まだ周囲のコードに漏れています。漏れない場合、通常、正当な理由がないようにランダムに壊れるアプリケーションをデバッグする必要があります。


では、GetSomethingAsyncが2秒で完了するスレッドをスピンアップしたらどうなるでしょうか。はい、明らかに新しいスレッドがあります。ただし、このスレッドはbecauseこのメソッドの非同期性ではありません。これは、このメソッドのプログラマーが非同期コードを実装するスレッドを選択したためです。ほとんどすべての非同期I/O do n'tはスレッドを使用しますが、異なるものを使用します。 async/awaitby alone新しいスレッドを起動しませんが、明らかに「待機するもの」はスレッドを使用して実装できます。

.NETには、必ずしもそれ自体でスレッドをスピンアップする必要はありませんが、依然として非同期であるものが多数あります。

  • Web要求(および時間がかかる他の多くのネットワーク関連のもの)
  • 非同期ファイルの読み取りと書き込み
  • さらに、問題のクラス/インターフェースにSomethingSomethingAsyncまたはBeginSomethingおよびEndSomethingという名前のメソッドがあり、IAsyncResultが関係している場合は、良い兆候です。

通常、これらのものはボンネットの下のスレッドを使用しません。


OK、だからあなたはその「幅広い話題のもの」のいくつかが欲しいですか?

さて、ボタンのクリックについて Try Roslyn を聞いてみましょう。

ロズリンを試す

ここでは、生成された完全なクラスにリンクするつもりはありませんが、かなりつまらないものです。

コンピューターが一度に複数のことを実行しているように見える唯一の方法は、(1)実際に一度に複数のことを実行し、(2)タスクをスケジュールしてタスクを切り替えてシミュレートすることです。したがって、async-awaitがそれらのどちらもしない場合

Awaitがそれらのneitherを行うわけではありません。 awaitの目的は、同期コードを魔法のように非同期にするではないことを忘れないでください。非同期コードを呼び出すときに同期コードを記述するために使用するのと同じ手法を使用して、を有効にします。 Awaitは約であり、高遅延操作を使用するコードを低遅延操作を使用するコードのように見せます。これらの高遅延操作はスレッド上にある場合があり、特殊な目的のハードウェア上にある場合があり、作業を小さな断片に分割し、後でUIスレッドで処理するためにメッセージキューに入れる場合があります。彼らは非同期を達成するためにsomethingを行っていますが、theyがそれを行っています。待つだけで、その非同期性を利用できます。

また、3番目のオプションが欠けていると思います。 1990年代初頭のWindowsの世界は、私たち高齢者(ラップミュージックを身につけた今日の子供たちが私の芝生を脱ぐなど)を覚えています。マルチCPUマシンもスレッドスケジューラもありませんでした。 2つのWindowsアプリを同時に実行するには、yieldを実行する必要がありました。マルチタスクはcooperativeでした。 OSは、実行するようにプロセスに通知し、動作に問題がある場合、他のすべてのプロセスが提供されないようにします。それは降伏するまで実行され、何とかOSが次に制御を戻すときに中断したところから再開する方法を知る必要があります。シングルスレッドの非同期コードは、「yield」の代わりに「await」を使用したものによく似ています。待機とは、「ここで中断した場所を覚えて、他の人にしばらく走らせます。待機しているタスクが完了したら、電話をかけ直して、中断したところから再開します」という意味です。 Windows 3日間のように、アプリの応答性がどのように向上するかがわかると思います。

メソッドの呼び出しとは、メソッドの完了を待つことを意味します

不足しているキーがあります。 メソッドは、作業が完了する前に戻ることができます。それがまさに非同期性の本質です。メソッドは戻り、「この作業は進行中です。完了したらどうするかを教えてください」という意味のタスクを返します。メソッドの処理は完了していません。が返されましたが

Await演算子の前に、完了後にを実行する作業があるが、戻り値と完了が非同期化されたという事実に対処するために、スイスチーズに通したスパゲッティのようなコードを記述する必要がありました。。 Awaitを使用すると、looksのようなコードを記述できます。リターンと完了は同期されますが、actuallyは同期されません。

82
Eric Lippert

私は私のブログ記事でそれを完全に説明しています スレッドはありません

要約すると、最近のI/OシステムはDMA(Direct Memory Access)を多用しています。ネットワークカード、ビデオカード、HDDコントローラ、シリアル/パラレルポートなどには専用の専用プロセッサがあります。これらのプロセッサはメモリバスに直接アクセスし、CPUとは完全に独立して読み取り/書き込みを処理します。 CPUは、データを含むメモリ内の位置をデバイスに通知するだけでよく、その後、デバイスが読み取り/書き込みが完了したことをCPUに通知する割り込みを発生させるまで、独自の処理を実行できます。

操作が実行中になると、CPUが行うべき作業がなくなり、スレッドもなくなります。

77
Stephen Cleary

誰かがこの質問をしてくれて本当に嬉しいです。なぜなら私はスレッドが同時実行性のために必要であると最も長い間思っていたからです。最初にイベントループを見たとき、それらは嘘だと思いました。私は自分自身に「このコードがシングルスレッドで実行される場合、このコードを同時に実行することはできない」と考えました。これはの後であることを覚えておいてください。私はすでに並行性と並列性の違いを理解するのに苦労してきました。

私自身の調査の結果、私はようやく足りないものを見つけました。 select() 。具体的には、IO多重化。さまざまなカーネルによって異なる名前で実装されています:select()poll()epoll()kqueue()。これらは システムコール です。実装の詳細は異なりますが、 ファイル記述子 のセットを渡して監視することができます。その後、監視されているファイル記述子の1つが変更されるまでブロックする別の呼び出しを行うことができます。

したがって、IOイベントのセット(メインイベントループ)を待って、最初に完了したイベントを処理してから、イベントループに制御を戻すことができます。すすいで繰り返します。

これはどのように作動しますか?まあ、簡単な答えはそれがカーネルとハードウェアレベルの魔法であるということです。 CPU以外にもコンピュータには多数のコンポーネントがあり、これらのコンポーネントは並行して動作できます。カーネルはこれらのデバイスを制御し、それらと直接通信して特定のシグナルを受信できます。

これらのIO多重化システムコールは、node.jsやTornadoのようなシングルスレッドイベントループの基本的な構成要素です。関数をawaitにすると、特定のイベント(その関数の完了)を監視してから、メインイベントループに制御が戻ります。あなたが見ているイベントが完了すると、関数は(やがて)中断したところから再開します。このように計算を中断して再開できるようにする関数は、 コルーチン と呼ばれます。

27
gardenhead

awaitおよびasyncはスレッドではなくTasksを使用します。

フレームワークはTaskobjectsの形式でいくつかの作業を実行する準備ができているスレッドのプールを持っています。Taskをプールに送信することは、空きを選択することを意味します既に存在する1タスクアクションメソッドを呼び出すスレッド。
Taskを作成することは、新しいオブジェクトを作成することです。新しいスレッドを作成するよりもはるかに高速です。

TaskContinuationを付けることが可能であると仮定すると、スレッドが終了すると実行される新しいTaskオブジェクトです。

async/awaitTasksを使用しているので、それらは使用していません新しいcreateスレッド。


割り込みプログラミング技法は現代のすべてのOSで広く使用されていますが、ここでは関連性があるとは思いません。
aysnc/awaitを使用すると、単一のCPUでCPU結合タスク並列実行(実際にはインターリーブ)することができます。
これは、OSがキューイングをサポートしているという事実だけでは説明できませんでしたIORP


前回コンパイラがasyncメソッドを DFA に変換したことを確認したとき、作業はステップに分割され、各ステップはawait命令で終了していました。
awaitTaskを開始し、それに続きを付けて次のステップを実行します。

概念の例として、これが疑似コードの例です。
わかりやすくするために、またすべての詳細を正確に覚えているわけではないため、事態は単純化されています。

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

それはこのようなものに変形します

int state = 0;

Task NeXTSTEP()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(NeXTSTEP());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(NeXTSTEP());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   NeXTSTEP();

1 実際には、プールはそのタスク作成ポリシーを持つことができます。

22
Margaret Bloom

私はEric LippertやLasse V. Karlsenと競合するつもりはありません、そして他の人たち、私はこの質問の別の面に注意を向けたいと思います。

awaitを単独で使用しても、アプリは魔法のように反応するわけではありません。あなたがUIスレッドから待っているメソッドであなたがすることが何でもブロックするならば、それはまだあなたのUIをnon-awaitableバージョンがするのと同じ方法でブロックするでしょう

新しいスレッドを生成するか、完了ポートのようなものを使用するように、待機メソッドを具体的に記述する必要があります(完了ポートが通知されるたびに、現在のスレッドで実行を返し、継続のために別のものを呼び出します)。しかし、この部分は他の答えでよく説明されています。

14
Andrew Savinykh

これは私がこれをすべて見る方法です、それは超技術的に正確ではないかもしれません、しかしそれは少なくとも私を助けます:)。

マシン上で発生する処理(計算)には、基本的に2つのタイプがあります。

  • cPUで発生した処理
  • 他のプロセッサ(GPU、ネットワークカードなど)で発生した処理をIOと呼びましょう。

そのため、コンパイル後に使用するオブジェクトに応じて(そしてこれは非常に重要です)ソースコードを書くとき、処理はCPU限界、またはIO限界になります。 =、そして実際には、両方の組み合わせにバインドできます。

いくつかの例:

  • FileStreamオブジェクト(これはStreamです)のWriteメソッドを使用すると、処理は1%CPUバウンド、99%IOバウンドとなります。
  • NetworkStreamオブジェクト(これはStreamです)のWriteメソッドを使用すると、処理は1%CPUバウンド、99%IOバウンドとなります。
  • Memorystreamオブジェクト(これはStreamです)のWriteメソッドを使用すると、処理は100%CPUバウンドになります。

そのため、オブジェクト指向プログラマーの視点から見ると、Streamオブジェクトに常にアクセスしていますが、その下で行われることはオブジェクトの最終的な型によって大きく異なる場合があります。

さて、物事を最適化するために、可能ならば必要ならば、コードを並列で実行できると便利なことがあります(非同期はWordを使わないことに注意してください)。

いくつかの例:

  • デスクトップアプリケーションでは、ドキュメントを印刷したいのですが、それを待つ必要はありません。
  • 私のWebサーバーは同時に多くのクライアントを処理し、それぞれが並行して自分のページを取得します(シリアル化されていません)。

非同期/待機する前に、これには2つの解決策がありました。

  • スレッド。 ThreadクラスとThreadPoolクラスを使用すると、比較的使いやすくなりました。 スレッドはCPUにのみ制限されます
  • "古い" Begin/End/AsyncCallback非同期プログラミングモデル。これは単なるモデルです。CPUなのかIOバインドなのかはわかりません。 SocketクラスまたはFileStreamクラスを見てみると、IO boundというクールなことになりますが、使用することはめったにありません。

Async/awaitはタスクの概念に基づく一般的なプログラミングモデルです。 CPUバウンドタスクではスレッドやスレッドプールよりも少し使いやすく、古いBegin/Endモデルよりもずっと使いやすいです。アンダーカバー、しかし、それは両方の "ちょうど"超洗練された機能満載のラッパーです。

つまり、本当の勝利はほとんどIO Boundタスクにある、CPUを使わないタスクですが、async/awaitはまだプログラミングモデルに過ぎず、役に立ちません。最終的にどのように/どこで処理が行われるのかを決定します。

それは、クラスがTaskオブジェクトを返すメソッド "DoSomethingAsync"を持っているからではないということを意味します。これはCPUバウンドであると推測できます(これはおそらく無駄です。 (キャンセルトークンパラメータを持たない)、またはIO Bound(これはおそらく必須)、またはその両方の組み合わせ(モデルはかなりウイルス性、結合性、そして潜在的な利点は、結局、スーパーミックスで、それほど明白ではないかもしれません。

したがって、私の例に戻ると、MemoryStreamでasync/awaitを使用してWrite操作を実行しても、CPUの使用量は制限されません(おそらくファイルとネットワークストリームでは恩恵を受けられないでしょう)。

12
Simon Mourier

他の答えをまとめる:

非同期/待機は、IOバインドされたタスクを使用することで、呼び出しスレッドのブロックを回避できるので、主に作成されます。それらの主な用途は、スレッドがIOバインド操作でブロックされることが望ましくないUIスレッドでの使用です。

非同期は独自のスレッドを作成しません。呼び出し側メソッドのスレッドは、待機メソッドが見つかるまで非同期メソッドを実行するために使用されます。同じスレッドは、非同期メソッド呼び出しを超えて呼び出しメソッドの残りの部分を実行し続けます。呼び出された非同期メソッド内では、待機状態から戻った後、スレッドプールからのスレッドで継続を実行できます。別のスレッドが表示される唯一の場所です。

2
vaibhav kumar

ボトムアップで説明しようと思います。たぶん誰かがそれを役に立つと思うでしょう。 PascalのDOSで簡単なゲームを作ったとき、私はそこにいました、それをやり直し、それを再発明しました(古き良き時代...)

だから...すべてのイベント駆動型アプリケーションでは、内部に次のようなイベントループがあります。

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

フレームワークは通常、この詳細をあなたから隠しますが、それはそこにあります。 getMessage関数は、イベントキューから次のイベントを読み取るか、マウスの移動、キーダウン、キーアップ、クリックなどのイベントが発生するまで待機します。次に、dispatchMessageはイベントを適切なイベントハンドラーにディスパッチします。次に、ループを終了してアプリケーションを終了する終了イベントが来るまで、次のイベントなどを待ちます。

イベントループはより多くのイベントをポーリングでき、UIは応答性を維持できるように、イベントハンドラーは高速で実行する必要があります。ボタンのクリックがこのような高価な操作をトリガーするとどうなりますか?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

コントロールが関数内にとどまり、10秒の操作が終了するまで、UIは応答しなくなります。この問題を解決するには、タスクを迅速に実行できる小さな部分に分割する必要があります。つまり、1つのイベントですべてを処理することはできません。作業のわずかな部分を実行してから、別のイベントをポストするをイベントキューに送信して、継続を要求します。

したがって、これを次のように変更します。

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

この場合、最初の反復のみが実行され、次の反復を実行するためにメッセージがイベントキューにポストされて戻ります。 postFunctionCallMessage疑似関数の例では、「この関数を呼び出す」イベントをキューに入れるため、イベントディスパッチャーはそれに到達したときに呼び出します。これにより、他のすべてのGUIイベントを処理しながら、長時間実行される作業の一部を継続的に実行できます。

この長時間実行タスクが実行されている限り、その継続イベントは常にイベントキューにあります。したがって、基本的に独自のタスクスケジューラを発明しました。キュー内の継続イベントは、実行中の「プロセス」です。実際、これはオペレーティングシステムが行うことですが、継続イベントの送信とスケジューラループへの戻りは、OSがコンテキストスイッチングコードを登録したCPUのタイマー割り込みを介して行われるため、気にする必要はありません。ただし、ここでは独自のスケジューラを作成しているので、気にする必要があります-これまでのところ。

そのため、GUIと並行して単一のスレッドで長時間実行されるタスクを小さなチャンクに分割し、継続イベントを送信することで実行できます。これは、Taskクラスの一般的な考え方です。これは作業の一部を表し、その上で.ContinueWithを呼び出すと、現在の部分が終了したときに次の部分として呼び出す関数を定義します(そしてその戻り値が継続に渡されます)。 Taskクラスはスレッドプールを使用します。スレッドプールでは、最初に示したのと同様の作業を待機する各スレッドにイベントループがあります。これにより、数百万のタスクを並行して実行できますが、実行するスレッドはわずかです。しかし、単一のスレッドでも同じように機能します。タスクが適切に小さな断片に分割されている限り、各タスクは並列で実行されているように見えます。

しかし、バックグラウンドタスクコード全体が基本的に.ContinueWith混乱であるため、作業を小さな部分に手動で分割するこの連鎖をすべて行うのは面倒な作業であり、ロジックのレイアウトを完全に混乱させます。そのため、ここでコンパイラが役立ちます。バックグラウンドでこのすべての連鎖と継続を行います。 awaitと言うとき、コンパイラに「ここでやめて、残りの関数を継続タスクとして追加する」と伝えます。コンパイラが残りの部分を処理するので、あなたはする必要はありません。

1
Calmarius

async awaitチェーンは実際にはCLRコンパイラによって生成されたステートマシンです。

async awaitは、TPLがタスクを実行するためにスレッドプールを使用しているスレッドを使用します。

アプリケーションがブロックされないのは、ステートマシンがどのコルーチンを実行し、繰り返し、チェックし、そして再度決定するかを決定できるからです。

参考文献:

async&awaitは何を生み出しますか?

非同期待機および生成されたStateMachine

非同期C#とF#(III。):どのように機能しますか? - Tomas Petricek

編集

はい。私の詳細が間違っているようです。しかし、ステートマシンはasync awaitsにとって重要な資産であることを指摘する必要があります。非同期I/Oを取り込んでも、操作が完了したかどうかを確認するためのヘルパーがまだ必要です。したがってステートマシンが必要で、どのルーチンを非同期に実行できるかを判断する必要があります。

0
Steve Fan