Подтвердить что ты не робот

Async ждет исполнения?

(Только теоретический вопрос - для не-gui-приложений)

Предполагая, что у меня есть этот код со многими awaits:

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

Если каждая задача может занять очень короткий промежуток времени,

Вопрос (опять же, теоретический)

Там может быть ситуация, когда общее время имеет дело со всеми этими "освобождением назад потоков" и "выборкой потоков назад" (красный и зеленый здесь:)

enter image description here

Занимает больше времени, чем один поток, который мог бы выполнить всю работу с небольшой задержкой,

Я имею в виду, я хотел быть самым продуктивным, но вместо этого, поскольку все эти переключатели взад и вперед - я действительно потерял производительность.

Может ли такой сценарий произойти?

4b9b3361

Ответ 1

Объект

A 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, если у вас есть выбор, а не по соображениям производительности, но для удобства использования, удобочитаемости и ремонтопригодности.

Ответ 2

Да, в теории. Обычно не в реальном мире.

В общем случае async используется для операций с привязкой к I/O, а накладные расходы на управление потоками необнаружимы по сравнению с ними. В большинстве случаев асинхронные операции занимают очень много времени (по сравнению с управлением потоками) или уже завершены (например, кеш). Обратите внимание, что async имеет "быстрый путь", который срабатывает, если операция уже завершена, где она не дает поток.

Для получения дополнительной информации см. Zen of Async и Async Performance.

Ответ 3

Может ли такой сценарий произойти?

Совершенно верно. По этой причине вы должны быть добросовестными в том, где вы используете асинхронный код. Как правило, вам лучше всего использовать его для методов, которые будут фактически выполнять асинхронную операцию (например, диск или сетевой ввод-вывод). Время, затрачиваемое этими операциями, обычно намного превышает стоимость планирования задач по потокам. Кроме того, на уровне операционной системы эти виды операций по своей сути являются асинхронными, поэтому вы фактически удаляете слой абстракции с помощью методов async.

Даже в этих случаях, скорее всего, вы не увидите заметной разницы в производительности, переключившись на асинхронный код, если только вы не сможете воспользоваться преимуществами concurrency. Например, код, который вы опубликовали, вероятно, не увидит реального прироста производительности, если он не был изменен на что-то вроде этого:

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

Ответ 4

Да, это может произойти. Также не забывайте, что - вся эффективность там, которую вы можете запрограммировать, - система задач имеет накладные расходы.

Если вы получаете слишком гранулкар с чем-то вроде этого, накладные расходы на синхронизацию могут убить вас. СЧИТАЕТ: Задачи довольно эффективно запрограммированы.

Но старое правило придерживается: не переходите супер гранулированным. Оптимизация SOmetimes помогает.

Ответ 5

Да, конечно, это может произойти. Со всеми накладными расходами на создание конечного автомата, возвращающего назад и вперед управление и использование потоков IOCP. Но, как сказано, TPL довольно оптимизирован. Например, не забывайте, что если ваш TaskAwaitable быстро завершается, накладные расходы могут быть несинхронными, что может происходить часто с быстрыми операциями.