web-dev-qa-db-ja.com

async / awaitのアーキテクチャ

アーキテクチャのより低いレベルでasync/awaitを使用している場合、async/await呼び出しを完全に「バブルアップ」する必要がありますか?基本的に各レイヤーに新しいスレッドを作成しているため、非効率的です(非同期に呼び出す各層の非同期関数、またはそれは本当に重要ではなく、単にあなたの好みに依存していますか?

私はEF 6.0-alpha3を使用しているため、EFで非同期メソッドを使用できます。

私のリポジトリはそのようなものです:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public async virtual Task Save()
    {
        await context.SaveChangesAsync();
    }
}

今私のビジネス層は次のとおりです:

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public async virtual Task Save()
    {
        await repository.Save();
    }
}

そしてもちろん、UIの私のメソッドは、呼び出し時に同じパターンに従います。

これは:

  1. 必要
  2. パフォーマンスが悪い
  3. 好みの問題

これが別のレイヤー/プロジェクトで使用されていない場合でも、同じクラスでネストされたメソッドを呼び出す場合、同じ質問が当てはまります。

    private async Task<string> Dosomething1()
    {
        //other stuff 
        ...
        return await Dosomething2();
    }
    private async Task<string> Dosomething2()
    {
        //other stuff 
        ...
        return await Dosomething3();
    }
    private async Task<string> Dosomething3()
    {
        //other stuff 
        ...
        return await Task.Run(() => "");
    }
58
valdetero

アーキテクチャのより低いレベルでasync/awaitを使用している場合、async/await呼び出しを完全に「バブルアップ」する必要がありますか?基本的に各レイヤーに新しいスレッドを作成しているため、非効率的です(非同期に呼び出す各レイヤーの非同期関数、またはそれは本当に重要ではなく、単にあなたの好みに依存していますか?

この質問は、いくつかの誤解の領域を示唆しています。

まず、非同期関数を呼び出すたびにしないでください新しいスレッドを作成します。

第2に、非同期関数を呼び出すからといって、非同期メソッドを宣言する必要はありません。既に返されているタスクに満足している場合は、しないにasync修飾子があるメソッドからそれを返すだけです:

public class EntityRepository<E> : IRepository<E> where E : class
{
    public virtual Task Save()
    {
        return context.SaveChangesAsync();
    }
}

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public virtual Task Save()
    {
        return repository.Save();
    }
}

このwillは、非常に小さな理由で作成された状態マシンを必要としないため、少し効率的ですが、さらに重要なことに、より簡単です。

awaitまたはTask<T>を待機する単一のTask式があり、メソッドの最後にそれ以上の処理が行われていない非同期メソッドは、使用せずに記述した方がよいでしょう。非同期/待機。したがって、この:

public async Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return await bar.Quux();
}

次のように書く方が良いです:

public Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return bar.Quux();
}

(理論的には、作成されるタスクに非常にわずかな違いがあるため、呼び出し元が継続を追加できるものもありますが、ほとんどの場合、違いに気付くことはありません。)

58
Jon Skeet

基本的に各レイヤーに新しいスレッドを作成しているので非効率ですか(各レイヤーの非同期関数を非同期に呼び出しますか、それとも本当に重要ではなく、設定に依存していますか?

いいえ。非同期メソッドは必ずしも新しいスレッドを使用する必要はありません。この場合、基になる非同期メソッド呼び出しはIOバインドメソッドであるため、実際には新しいスレッドは作成されません。

これは:

_1. necessary
_

操作を非同期にしたい場合は、非同期呼び出しを「バブル」する必要があります。ただし、スタック全体でそれらを一緒に構成することを含め、非同期メソッドを十分に活用できるため、これは本当に推奨されます。

_2. negative on performance
_

いいえ、言ったように、これは新しいスレッドを作成しません。オーバーヘッドは多少ありますが、その多くは最小限に抑えることができます(以下を参照)。

_3. just a matter of preference
_

これを非同期に保ちたい場合はそうではありません。スタック全体で非同期にするために、これを行う必要があります。

これで、パフォーマンスを向上させるためにできることがいくつかあります。ここに。非同期メソッドをラップするだけの場合は、言語機能を使用する必要はありません。Taskを返すだけです。

_public virtual Task Save()
{
    return repository.Save();
}
_

repository.Save()メソッドはすでにTaskを返します-Taskにラップするためだけにそれを待つ必要はありません。これにより、メソッドの効率がいくらか向上します。

「低レベル」の非同期メソッドで ConfigureAwait を使用して、呼び出し側の同期コンテキストが不要になるようにすることもできます。

_private async Task<string> Dosomething2()
{
    //other stuff 
    ...
    return await Dosomething3().ConfigureAwait(false);
}
_

これにより、各awaitに関連するオーバーヘッドが劇的に減少します呼び出しコンテキストを気にする必要がない場合。 「外部」awaitはUIのコンテキストをキャプチャするため、これは通常「ライブラリ」コードで作業する場合の最良のオプションです。ライブラリの「内部」の仕組みは通常、同期コンテキストを気にしないので、それをキャプチャしないことが最善です。

最後に、私はあなたの例の1つに対して警告します:

_private async Task<string> Dosomething3()
{
    //other stuff 
    ...
    // Potentially a bad idea!
    return await Task.Run(() => "");
}
_

内部で_Task.Run_を使用して、それ自体が非同期ではないものの周りに「非同期を作成」する非同期メソッドを作成している場合、同期コードを非同期メソッドに効果的にラップしています。このwillはThreadPoolスレッドを使用しますが、そうしているという事実を「隠す」ことができ、APIを事実上誤解させることがあります。最高レベルの呼び出しのために_Task.Run_への呼び出しを残し、非同期IOまたはアンロード以外のなんらかの手段を真に利用できない限り、基礎となるメソッドを同期させたままにする方がよい場合がよくあります。 _Task.Run_。 (これはalways trueではありませんが、「非同期」コードが_Task.Run_を介して同期コードにラップされ、次にasync/awaitを介して返される場合は、多くの場合、設計に欠陥があることを示しています。)

28
Reed Copsey