awaitを使用することと、非同期タスクを処理するときにContinueWithを使用することとの違いは何ですか?


8

これが私の意味です:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(new SomeObject()
            {
                IsAuthorized = false
            });
        }
        else
        {
            return repository.GetSomeObjectByTokenAsync(token).ContinueWith(t =>
            {
                t.Result.IsAuthorized = true;
                return t.Result;
            });
        }
    }

上記の方法は待つことができますが、T askベースのA同期P atternが提案することとよく似ていると思いますか?(私が知っている他のパターンはAPMおよびEAPパターンです。)

では、次のコードはどうでしょう。

public async Task<SomeObject> GetSomeObjectByToken(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return new SomeObject()
            {
                IsAuthorized = false
            };
        }
        else
        {
            SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
            result.IsAuthorized = true;
            return result;
        }
    }

主な違いはここに方法があるということですasync、それは利用しawaitたキーワードを-ので、以前に書かれた方法とは対照的に、この変更を何?私もそれができることを知っています-待ってください。Taskを返すメソッドはすべて、私が間違っていない限り、そのことについてはできます。

メソッドがとラベル付けされているときはいつでも、これらのswitchステートメントで作成されたステートマシンをasync認識しawaitています。また、それ自体はスレッドを使用しないことを認識しています。スレッドはまったくブロックされません。上記のコードの実行を継続するためにコールバックされました。

しかし、awaitキーワードを使用して呼び出す場合、2つのメソッドの根本的な違いは何ですか?何か違いはありますか、そしてある場合、どちらが好ましいですか?

編集:私は最初のコードスニペットが好まれているように感じます、何の影響もなく、非同期/待機キーワードを効果的に除外するためです-同期して実行を継続するタスク、またはホットパスですでに完了したタスク(キャッシュされます)。


3
この場合、ごくわずかです。最初の例でresult.IsAuthorized = trueは、はスレッドプールで実行されますが、2番目の例では、呼び出されたのと同じスレッドで実行されますGetSomeObjectByTokenSynchronizationContextUIスレッドなどがインストールされている場合)。GetSomeObjectByTokenAsync例外をスローした場合の動作も少し異なります。ほとんどの場合、より読みやすいので、一般にawaitが優先されContinueWithます。
canton7

1
、この記事で、それはそれのためのかなり良いの言葉のように思われる「eliding」と呼ばれています。スティーブンは間違いなく彼のビジネスを知っています。この記事の結論は基本的に、パフォーマンスに関してはそれほど重要ではないということですが、待たなければ、特定のコーディングの落とし穴があります。2番目の例は、より安全なパターンです。
John Wu

1
ASP.NETチームのMicrosoftのパートナーソフトウェアアーキテクトによるこのTwitterディスカッションに興味があるかもしれません。twitter.com
Remy

これは「仮想マシン」ではなく、「ステートマシン」と呼ばれます。
Enigmativity

@エニグマティティ賢明な方ありがとうございます、編集するのを忘れました!
SpiritBob

回答:


5

async/ awaitメカニズムは、コンパイラが、状態マシンにあなたのコードを変換することができます。コードはawait、もしあれば完了していないawaitableに最初に到達するまで同期的に実行されます。

Microsoft C#コンパイラでは、このステートマシンは値型です。つまりawait、オブジェクトが割り当てられないため、すべてのが待機可能オブジェクトを完了すると、ごみが生成されないため、コストは非常に小さくなります。待機可能なものが完了しない場合、この値タイプは必然的にボックス化されます。

Task式で使用されるawaitableのタイプである場合、これはsの割り当てを回避しないことに注意してくださいawait

を使用するとContinueWithTask継続にクロージャがない場合、および状態オブジェクトを使用しないか、状態オブジェクトを可能な限り再利用する場合(プールなど)にのみ、(以外の)割り当てを回避できます。

また、継続はタスクが完了したときに呼び出され、スタックフレームが作成されます。インライン化されません。フレームワークはスタックオーバーフローを回避しようとしますが、大きな配列がスタックに割り当てられている場合など、回避できない場合があります。

これを回避しようとする方法は、残っているスタックの量をチェックすることであり、内部測定によってスタックがいっぱいであると見なされた場合、タスクスケジューラで実行するように継続をスケジュールします。パフォーマンスを犠牲にして、致命的なスタックオーバーフロー例外を回避しようとします。

async/ awaitとの微妙な違いはContinueWith次のとおりです。

  • async/ await継続があるSynchronizationContext.Current場合はそれをスケジュールし、それ以外の場合は1にスケジュールしますTaskScheduler.Current

  • ContinueWith提供されたタスクスケジューラ、またはTaskScheduler.Currentタスクスケジューラパラメータなしのオーバーロードで継続をスケジュールします

async/ awaitのデフォルトの動作をシミュレートするには:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)

シミュレートするために、async/ await「での行動TaskS」.ConfigureAwait(false)

.ContinueWith(continuationAction,
    TaskScheduler.Default)

ループと例外処理が複雑になり始めています。あなたのコードの読みを保つだけでなく、async/ awaitいずれかで動作しますawaitable

あなたのケースは混合アプローチで最もよく処理されます:必要なときに非同期メソッドを呼び出す同期メソッド。このアプローチを使用したコードの例:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
    string token = repository.GetTokenById(id);
    if (string.IsNullOrEmpty(token))
    {
        return Task.FromResult(new SomeObject()
        {
            IsAuthorized = false
        });
    }
    else
    {
        return InternalGetSomeObjectByTokenAsync(repository, token);
    }
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
    SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
    result.IsAuthorized = true;
    return result;
}

私の経験では、私は非常に少数の場所を見つけたアプリケーションなどの複雑さを追加すると、実際には、一方、レビューとテストこのようなアプローチを開発するための時間報わコードライブラリのコードのいずれかの方法がボトルネックになることができます。

私がタスクを除外する唯一のケースは、Taskまたはreturn Task<T>メソッドが、それ自体がI / Oや後処理を実行せずに、単に別の非同期メソッドの結果を返す場合です。

YMMV。


  1. ConfigureAwait(false)カスタムスケジューリングを使用するawaitable を使用または待機しない限り

1
状態マシンは値タイプではありません。生成された単純な非同期プログラムのソースを見てみましょうprivate sealed class <Main>d__0 : IAsyncStateMachine。ただし、このクラスのインスタンスは再利用できます。
Theodor Zoulias

タスクの結果のみを改ざんするのであれば、使用しても大丈夫だと思いますContinueWithか?
SpiritBob

3
@ TheodorZoulias、DebugではなくReleaseをターゲットとして生成されたソースは構造体を生成します。
-acelent

2
@SpiritBobは、タスクの結果をいじる場合は問題ないと思いますが、.ContinueWithプログラムで使用するためのものでした。特に、I / OタスクではなくCPUを集中的に使用するタスクを処理している場合はそうです。しかし、Taskのプロパティ、AggregateExceptionおよび条件、サイクル、さらに非同期メソッドが呼び出されたときの例外処理の処理の複雑さを処理する必要がある場合、それに固執する理由はほとんどありません。
-acelent

2
@SpiritBob、申し訳ありませんが、私は「ループ」を意味しました。はい、静的読み取り専用フィールドをで作成できますTask.FromResult("...")。非同期コードでは、取得にI / Oを必要とするキーで値をキャッシュする場合、値がタスクであるディクショナリを使用できます。たとえば、ConcurrentDictionary<string, Task<T>>代わりにConcurrentDictionary<string, T>GetOrAddファクトリ関数での呼び出しを使用して、その結果を待ちます。これにより、キャッシュのキーにデータを入力するためのI / O要求が1つだけ行われることが保証され、タスクが完了するとすぐにウェイターが起動し、その後完了したタスクとして機能します。
acelent

2

使用することでContinueWith利用可能な場合の導入前というツールを使って、あなたをしているasync/のawaitそれは、冗長簡単に構成可能ではない、とアンラップのための余分な作業を必要とし、2012年にC#5バックと機能ツールとしてAggregateExceptionSをしてTask<Task<TResult>>(あなたはときに、これらを取得する戻り値非同期デリゲートを引数として渡します)。その見返りとして、いくつかの利点があります。あなたが同じに複数の継続を添付したいときにそれを使用して検討することTask、または使用することはできませんいくつかのまれなケースではasync/ await(あなたがメソッドにいるときのようないくつかの理由outパラメータ)。


更新:ContinueWith使用しTaskScheduler.Defaultてのデフォルトの動作を模倣する必要があるという誤解を招くようなアドバイスを削除しましたawait実際awaitデフォルトでは、を使用して継続をスケジュールしますTaskScheduler.Current

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.