Чем использование await отличается от использования ContinueWith при обработке асинхронных задач? - PullRequest
8 голосов
/ 31 октября 2019

Вот что я имею в виду:

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 на основе запроса 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? Есть ли какая-то разница, и если есть, то какая предпочтительнее?

РЕДАКТИРОВАТЬ: Мне кажется, что первый фрагмент кода предпочтительнее, потому что мы эффективно elide ключевые слова async / await, без каких-либо последствий - мы возвращаем задачу, которая будетпродолжить выполнение синхронно или уже выполненную задачу по горячему пути (которую можно кэшировать).

Ответы [ 2 ]

5 голосов
/ 04 ноября 2019

Механизм async / await позволяет компилятору преобразовать ваш код в конечный автомат. Ваш код будет работать синхронно до тех пор, пока первый await не достигнет ожидающего, который еще не завершен, если таковой имеется.

В компиляторе Microsoft C # этот конечный автомат является типом значения, что означает, что он будет иметь оченьнебольшая стоимость, когда все await s получат завершенные ожидаемые объекты, поскольку они не будут выделять объект и, следовательно, не будут создавать мусор. Если какое-либо ожидаемое не завершено, этот тип значения неизбежно помещается в квадрат.

Обратите внимание, что это не исключает выделения Task с, если это тип ожидаемых, используемых в выражениях await.

С ContinueWith вы избегаете выделений (кроме Task), только если у вашего продолжения нет замыкания и если вы либо не используете объект состояния, либо повторно используете состояниеОбъект как можно больше (например, из пула).

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

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

Здесь есть небольшая разница между async / await и ContinueWith:

  • async / await запланирует продолжения в SynchronizationContext.Current, если таковые имеются, в противном случае в TaskScheduler.Current 1

  • ContinueWith запланирует продолжения в предоставленномпланировщик задач или в TaskScheduler.Current в перегрузках без параметра планировщика задач

Для имитации поведения async / await по умолчанию:

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

Для имитации поведения async / await с Task * .ConfigureAwait(false):

.ContinueWith(continuationAction,
    TaskScheduler.Default)

Вещи начинают усложняться циклами и обработкой исключений. Помимо обеспечения читабельности вашего кода, async / await работает с любыми ожидаемыми .

Ваш случай лучше всего обрабатывать смешанным подходом: синхронный метод, который при необходимости вызывает асинхронный метод,Пример вашего кода с таким подходом:

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;
}

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

Единственный случай, когда я склонен выполнять задачи elide, - это когда Task или Task<T>Метод return просто возвращает результат другого асинхронного метода, не выполнив сам ввод-вывод или пост-обработку.

YMMV.


  1. Если вы не используете ConfigureAwait(false) или ждите какой-нибудь ожидающей, которая использует пользовательское планирование
2 голосов
/ 31 октября 2019

Используя ContinueWith, вы используете инструменты, которые были доступны до введения функциональности async / await с C # 5 еще в 2012 году. Как инструмент, он многословен, не так легко компонуется и требует дополнительныхработать для разворачивания возвращаемых значений AggregateException s и Task<Task<TResult>> (вы получаете их, когда передаете асинхронные делегаты в качестве аргументов). Он предлагает несколько преимуществ взамен. Вы можете использовать его, когда хотите присоединить несколько продолжений к одному и тому же Task, или в некоторых редких случаях, когда по какой-то причине вы не можете использовать async / await (например, когда находитесь в методе * 1009). * с out параметрами ).


Обновление: Я удалил вводящий в заблуждение совет о том, что ContinueWith следует использовать TaskScheduler.Default для имитации поведения по умолчаниюawait. На самом деле await по умолчанию планирует его продолжение, используя TaskScheduler.Current.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...