web-dev-qa-db-ja.com

非同期はパフォーマンスを待ちますか?

(ちょうど理論的な質問-非GUIアプリの場合)

私が多くのawaitsでこのコードを持っていると仮定します:

public async Task<T> ConsumeAsync()
    {
          await A();
          await b();
          await c();
          await d();
          //..
    }

各タスクに非常に短い時間がかかる場合、

質問(ここでも、理論上の)

そこにすることができますすべての時間を処理する全体的な時間スレッドの解放」と「スレッドのフェッチ」(ここでは赤と緑:)

enter image description here

少しの遅延ですべての作業を実行できる単一のスレッドよりもより多くの時間を要している、

つまり、私は最も生産的になりたかったのですが、その代わりに、これらすべてのスイッチが前後するため、実際には生産性が失われました。

そのようなシナリオは起こり得ますか?

30
Royi Namir

Taskオブジェクトは、保留中の操作の遅延結果を表します。保留中の操作がない場合は、タスクとasync/awaitを使用する必要はありません。それ以外の場合、async/awaitコードは通常、そのTPL ContinueWithアナログよりも効率的だと思います。

いくつかのタイミングをやってみましょう:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    class Program
    {
        // async/await version
        static async Task<int> Test1Async(Task<int> task)
        {
            return await task;
        }

        // TPL version
        static Task<int> Test2Async(Task<int> task)
        {
            return task.ContinueWith(
                t => t.Result,
                CancellationToken.None,
                TaskContinuationOptions.ExecuteSynchronously,
                TaskScheduler.Default);
        }

        static void Tester(string name, Func<Task<int>, Task<int>> func)
        {
            var sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            for (int i = 0; i < 10000000; i++)
            {
                func(Task.FromResult(0)).Wait();
            }
            sw.Stop();
            Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
        }

        static void Main(string[] args)
        {
            Tester("Test1Async", Test1Async);
            Tester("Test2Async", Test2Async);
        }
    }
}

出力:

 Test1Async:1582ms 
 Test2Async:4975ms 

したがって、デフォルトでは、await継続はContinueWith継続よりも効率的に処理されます。このコードを少し最適化しましょう:

// async/await version
static async Task<int> Test1Async(Task<int> task)
{
    if (task.IsCompleted)
        return task.Result;
    return await task;
}

// TPL version
static Task<int> Test2Async(Task<int> task)
{
    if (task.IsCompleted)
        return Task.FromResult(task.Result);

    return task.ContinueWith(
        t => t.Result,
        CancellationToken.None,
        TaskContinuationOptions.ExecuteSynchronously,
        TaskScheduler.Default);
}

出力:

 Test1Async:1557ms 
 Test2Async:429ms 

これで、非同期バージョンが優先されます。 asyncバージョンの場合、この最適化はasync/awaitインフラストラクチャによって既に内部的に行われていると思います。

とにかく、これまでは完了したタスク(Task.FromResult)のみを扱いました。実際の非同期性を紹介しましょう(当然、今回は反復回数を減らします):

static Task<int> DoAsync()
{
    var tcs = new TaskCompletionSource<int>();
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(0));
    return tcs.Task;
}

static void Tester(string name, Func<Task<int>, Task<int>> func)
{
    ThreadPool.SetMinThreads(200, 200);
    var sw = new System.Diagnostics.Stopwatch();
    sw.Start();
    for (int i = 0; i < 1000000; i++)
    {
        func(DoAsync()).Wait();
    }
    sw.Stop();
    Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
}

出力:

 Test1Async:4207ms 
 Test2Async:4734ms 

asyncバージョンのパフォーマンスはやや向上しますが、違いはごくわずかです。それでも、このような利益は無視できると思います。これは、非同期操作の実際のコスト、またはSynchronizationContext.Current != nullの場合にキャプチャされたコンテキストを復元するコストに匹敵します。

肝心なのは、非同期タスクを扱う場合、パフォーマンス上の理由ではなく、使いやすさ、読みやすさ、保守性から選択できる場合はasync/awaitを選択することです。

14
noseratio

はい、理論的には。通常は、現実の世界ではありません。

一般的なケースでは、asyncはI/Oバウンド操作に使用され、スレッド管理のオーバーヘッドはそれらと比較して検出できません。ほとんどの場合、非同期操作は(スレッド管理と比較して)非常に長い時間がかかるか、すでに完了しています(キャッシュなど)。 asyncには、操作がすでに完了していてスレッドを生成しない場合に作動する「高速パス」があることに注意してください。

詳細については、 Zen of Async および Async Performance を参照してください。

20
Stephen Cleary

そのようなシナリオは起こり得ますか?

もちろんです。このため、非同期コードをどこで使用するかを慎重に検討する必要があります。通常は、実際に非同期操作を実行するメソッド(ディスクまたはネットワークI/Oなど)に使用するのが最適です。これらの操作にかかる時間は、通常、スレッドでタスクをスケジュールするコストをはるかに上回ります。また、オペレーティングシステムレベルでは、これらの種類の操作は本質的に非同期であるため、実際には削除非同期メソッドを使用した抽象化の層です。

ただし、これらの場合でも、同時実行性を利用できない限り、非同期コードに切り替えても顕著なパフォーマンスの違いは見られません。たとえば、投稿したコードは、次のように変更しない限り、実際のパフォーマンスは向上しません。

await Task.WhenAll(new[]{A(), B(), C(), D(), ...});
2

はい、起こり得ます。また、そのことを忘れないでください-その中でプログラミングできるすべての効率-タスクシステムにはオーバーヘッドがあります。

このようなものでグラニュラカーが多すぎると、同期のオーバーヘッドがあなたを殺します。それは言った:タスクは非常に効率的にプログラムされています。

しかし、古いルールは固執します。 SOmetimesの最適化が役立ちます。

2
TomTom

はい、もちろんそれは起こり得ます。ステートマシンの作成に伴うすべてのオーバーヘッドにより、制御をやり取りし、IOCPスレッドを使用します。しかし、言ったように、TPLはかなり最適化されています。たとえば、TaskAwaitableがすぐに終了する場合、オーバーヘッドがなく、同期して実行されることを忘れないでください。これは、迅速な操作で頻繁に発生する可能性があります。

1
Yuval Itzchakov