web-dev-qa-db-ja.com

awaitとContinueWithの違い

次の例で、awaitContinueWithが同義かどうかを説明できますか。私は初めてTPLを使用しようとしており、すべてのドキュメントを読んでいますが、違いを理解していません。

待機

String webText = await getWebPage(uri);
await parseData(webText);

ContinueWith

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

特定の状況では、一方が他方よりも優先されますか?

104
Harrison

2番目のコードでは、継続が完了するまで同期待機しています。最初のバージョンでは、メソッドは、まだ完了していない最初のawait式に到達するとすぐに呼び出し元に戻ります。

どちらも継続をスケジュールするという点で非常に似ていますが、制御フローが少し複雑になるとすぐに、awaitmuchより単純なコードになります。さらに、コメントでServyが指摘したように、タスクを待機すると、例外が集約され、通常はより簡単なエラー処理が行われます。また、awaitを使用すると、呼び出しコンテキストでの継続が暗黙的にスケジュールされます(ConfigureAwaitを使用する場合を除く)。 「手動で」実行できないことは何もありませんが、awaitを使用すると簡単に実行できます。

awaitTask.ContinueWithの両方を使用して、少し大きな操作シーケンスを実装してみることをお勧めします。これは本当に目を見張るものになります。

88
Jon Skeet

以下は、非同期ソルブを使用して違いとさまざまな問題を説明するために最近使用したコードスニペットのシーケンスです。

GUIベースのアプリケーションに多くの時間がかかるイベントハンドラーがあり、非同期にしたいとします。同期ロジックは次のとおりです。

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItemはTaskを返し、最終的に検査したい結果を生成します。現在の結果が探しているものである場合、UI上のカウンターの値を更新し、メソッドから戻ります。それ以外の場合は、LoadNextItemからさらにアイテムを処理し続けます。

非同期バージョンの最初のアイデア:継続を使用するだけです!とりあえずループ部分を無視しましょう。つまり、何が間違っているのでしょうか?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

素晴らしい、今ではブロックしないメソッドがあります!代わりにクラッシュします。 UIコントロールの更新はUIスレッドで行われる必要があるため、そのことを考慮する必要があります。ありがたいことに、継続をスケジュールする方法を指定するオプションがあり、これだけのデフォルトがあります。

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

素晴らしい、今ではクラッシュしないメソッドがあります!代わりに静かに失敗します。継続はそれ自体が別個のタスクであり、それらのステータスは先行タスクのステータスに関連付けられていません。そのため、LoadNextItemに障害が発生した場合でも、呼び出し元には正常に完了したタスクのみが表示されます。さて、例外があれば、それを渡してください:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

素晴らしい、今これは実際に動作します。単一のアイテムの場合。では、ループについてはどうでしょう。元の同期バージョンのロジックと同等のソリューションは、次のようになります。

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

または、上記のすべての代わりに、非同期を使用して同じことを行うことができます。

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

今ではもっといいですね。

86
pkt