35

ConfigureAwait(false)C# で await/async を使用する場合、 をいつ使用するかについては、多くのガイドラインがあります。

ConfigureAwait(false)同期コンテキストに依存することはめったにないため、ライブラリコードで使用することが一般的に推奨されているようです。

ただし、関数を入力として受け取る非常に一般的なユーティリティ コードを作成しているとします。簡単な例としては、単純なタスクベースの操作を簡単にするための次の (不完全な) 関数コンビネータがあります。

地図:

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping)
{
    return mapping(await task);
}

フラットマップ:

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping)
{
    return await mapping(await task);
}

問題は、ConfigureAwait(false)この場合に使用する必要があるかどうかです。コンテキストキャプチャがどのように機能するかわかりません。閉鎖。

一方では、コンビネータが機能的な方法で使用される場合、同期コンテキストは必要ありません。一方、人々は API を誤用し、提供された関数でコンテキスト依存のことを行う可能性があります。

1 つのオプションは、各シナリオ (MapおよびMapWithContextCaptureまたは何か) ごとに個別のメソッドを用意することですが、見苦しく感じます。

別のオプションは、マップ/フラットマップからおよびへのオプションを追加することかもしれませんConfiguredTaskAwaitable<T>が、awaitables はインターフェイスを実装する必要がないため、多くの冗長なコードが発生し、私の意見ではさらに悪化します。

実装されたライブラリが提供されたマッピング関数でコンテキストが必要かどうかについて仮定をする必要がないように、責任を呼び出し元に切り替える良い方法はありますか?

それとも、さまざまな仮定がないと、非同期メソッドがうまく構成されないというのは単なる事実ですか?

編集

いくつかのことを明確にするために:

  1. 問題存在します。ユーティリティ関数内で「コールバック」を実行すると、 を追加ConfigureAwait(false)すると null 同期が発生します。環境。
  2. 主な問題は、この状況にどのように取り組むべきかということです。誰かが同期を使用する可能性があるという事実を無視する必要があります. または、オーバーロードやフラグなどを追加する以外に、責任を呼び出し元に移す良い方法はありますか?

いくつかの回答が言及しているように、メソッドにbool-flagを追加することは可能ですが、私が見るように、APIを介して伝播する必要があるため、これもあまりきれいではありません(上記のものに応じて、より多くの「ユーティリティ」機能)。

4

3 に答える 3

14

await task.ConfigureAwait(false)スレッドプールに移行すると、前のコンテキストで実行するのではmappingなく、null コンテキストで実行されます。これにより、異なる動作が発生する可能性があります。したがって、呼び出し元が次のように書いた場合:

await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...

Map次に、これは次の実装でクラッシュします。

var result = await task.ConfigureAwait(false);
return await mapper(result);

しかし、ここではありません:

var result = await task/*.ConfigureAwait(false)*/;
...

さらに恐ろしい:

var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);
...

同期コンテキストについてコインを投げてください! これはおかしく見えますが、見た目ほどばかげているわけではありません。より現実的な例は次のとおりです。

var result =
  someConfigFlag ? await GetSomeValue<T>() :
  await task.ConfigureAwait(false);

そのため、外部状態によっては、メソッドの残りの部分が実行される同期コンテキストが変化する可能性があります。

これは、次のような非常に単純なコードでも発生する可能性があります。

await someTask.ConfigureAwait(false);

await の時点で がすでに完了している場合someTask、コンテキストの切り替えはありません (これはパフォーマンス上の理由から良いことです)。切り替えが必要な場合、残りのメソッドはスレッド プールで再開されます。

この非決定性は、 の設計の弱点ですawait。これは、パフォーマンスという名のトレードオフです。

ここで最も厄介な問題は、API を呼び出すときに何が起こるかが明確でないことです。これは紛らわしく、バグの原因になります。

何をすべきか?

代替案 1:常に を使用して決定論的な動作を確保するのが最善であると主張できますtask.ConfigureAwait(false)

ラムダは、適切なコンテキストで実行されるようにする必要があります。

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext;
Map(..., async x => await Task.Factory.StartNew(
        () => { /*access UI*/ },
        CancellationToken.None, TaskCreationOptions.None, uiScheduler));

ユーティリティ メソッドでこれの一部を非表示にするのがおそらく最善です。

代替案 2:Map関数は同期コンテキストにとらわれないようにする必要があると主張することもできます。それは放っておくべきです。コンテキストはラムダに流れます。もちろん、同期コンテキストが存在するだけでMap(この特定のケースではなく、一般的に) の動作が変わる可能性があります。したがってMap、それを処理するように設計する必要があります。

代替案 3:Mapコンテキストをフローするかどうかを指定するブール値パラメーターを挿入できます。これにより、動作が明確になります。これは適切な API 設計ですが、API が乱雑になります。Map同期コンテキストの問題など、基本的な API に関心を持つのは適切ではないようです。

どのルートを取る?具体的なケースによると思います。たとえば、Mapが UI ヘルパー関数である場合、コンテキストをフローするのは理にかなっています。それがライブラリ関数 (再試行ヘルパーなど) であるかどうかはわかりません。すべての選択肢が理にかなっていることがわかります。通常、すべてのライブラリ コードに適用ConfigureAwait(false)することをお勧めします。ユーザー コールバックを呼び出す場合に例外を設ける必要がありますか? すでに適切なコンテキストを離れている場合はどうでしょうか。たとえば:

void LibraryFunctionAsync(Func<Task> callback)
{
    await SomethingAsync().ConfigureAwait(false); //Drops the context (non-deterministically)
    await callback(); //Cannot flow context.
}

残念ながら、簡単な答えはありません。

于 2015-05-04T18:30:37.280 に答える
8

問題は、この場合、ConfigureAwait(false) を使用する必要があるかどうかです。

はい、そうすべきです。待機中の内部Taskがコンテキストを認識しており、特定の同期コンテキストを使用している場合、それを呼び出している人が を使用していても、それをキャプチャできConfigureAwait(false)ます。コンテキストを無視するときは、提供されたデリゲート内ではなく、より高いレベルの呼び出しでそうしていることを忘れないでください。内で実行されるデリゲートはTask、必要に応じて、コンテキストを認識する必要があります。

呼び出し元であるあなたはコンテキストに関心がないため、 で呼び出してもまったく問題ありませんConfigureAwait(false)。これはあなたが望むことを効果的に行います。内部デリゲートに同期コンテキストを含めるかどうかの選択は、Mapメソッドの呼び出し元に任せます。

編集:

注意すべき重要なことは、一度使用するConfigureAwait(false)と、その後のメソッドの実行は任意のスレッドプール スレッドで行われるということです。

bool@i3arnon によって提案された良いアイデアは、コンテキストが必要かどうかを示すオプションのフラグを受け入れることです。少し醜いですが、うまく回避できます。

于 2015-05-04T17:59:11.783 に答える
6

ここでの本当の問題は、実際にその結果を操作している間に操作を追加しているという事実にあると思いますTask

これらの操作をタスクの結果に保持するのではなく、コンテナとしてタスクに複製する本当の理由はありません。

そうすればawait、ユーティリティ メソッドでこのタスクを実行する方法を決定する必要がなくなります。その決定はコンシューマ コードにとどまります。

IfMapは代わりに次のように実装されます。

public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping)
{
    return mapping(value);
}

Task.ConfigureAwaitそれに応じて、またはなしで簡単に使用できます。

var result = await task.ConfigureAwait(false)
var mapped = result.Map(result => Foo(result));

Mapこれはほんの一例です。ポイントは、ここで何を操作しているかです。タスクを操作している場合はawait、結果をコンシューマー デリゲートに渡す必要はありません。asyncロジックを追加するだけで、呼び出し元は使用するかどうかを選択できますTask.ConfigureAwait。結果を操作している場合は、心配する必要はありません。

これらのメソッドのそれぞれにブール値を渡して、キャプチャされたコンテキストを続行するかどうかを示すことができます (または、enum他の構成をサポートするオプション フラグをさらに確実に渡しawaitます)。Mapしかし、これは(またはそれに相当するもの)とは何の関係もないため、関心の分離に違反しています。

于 2015-05-04T17:59:34.153 に答える