Архитектура для async/wait

Если вы используете async/await на более низком уровне в своей архитектуре, необходимо ли "всплывать" вызовы async/await полностью, это неэффективно, поскольку вы в основном создаете новый поток для каждого уровня (асинхронно вызывающий асинхронная функция для каждого уровня, или это не имеет особого значения и зависит только от ваших предпочтений?

Я использую EF 6.0-alpha3, чтобы у меня были методы async в 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();
    }
}

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

Это:

  • необходимо
  • отрицательный результат
  • только вопрос предпочтения

Даже если это не используется в отдельных слоях/проектах, то одни и те же вопросы касаются, если я вызываю вложенные методы в одном классе:

    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(() => "");
    }

Ответ 1

Если вы используете async/await на более низком уровне в своей архитектуре, необходимо ли "всплывать" вызовы async/await полностью, неэффективно, поскольку вы в основном создаете новый поток для каждого уровня (асинхронно вызывая асинхронную функцию для каждого уровня, или это не имеет особого значения и зависит только от ваших предпочтений?

Этот вопрос предлагает пару областей недоразумений.

Во-первых, вы не создаете новый поток каждый раз, когда вы вызываете асинхронную функцию.

Во-вторых, вам не нужно объявлять метод async, только потому, что вы вызываете асинхронную функцию. Если вы довольны заданием, которое уже возвращается, просто верните это из метода, который не имеет модификатора 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();
    }
}

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

Любой метод async, в котором у вас есть одно выражение await, ожидающее Task или Task<T>, прямо в конце метода без дальнейшей обработки, лучше будет писать без использования async/await. Итак:

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

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

Ответ 2

это неэффективно, поскольку вы в основном создаете новый поток для каждого уровня (асинхронно вызывая асинхронную функцию для каждого уровня или это не имеет особого значения и зависит только от ваших предпочтений?)

Нет. Асинхронные методы не обязательно используют новые потоки. В этом случае, поскольку основной асинхронный вызов метода является методом привязки IO, на самом деле не должно быть никаких новых потоков.

Это:

1. necessary

Для асинхронных вызовов необходимо "пузырить" вызовы, если вы хотите, чтобы операция была асинхронной. Однако это действительно предпочтительнее, поскольку позволяет полностью использовать асинхронные методы, в том числе составлять их вместе во всем стеке.

2. negative on performance

Нет. Как я уже говорил, это не создает новые потоки. Есть некоторые накладные расходы, но многое из этого можно свести к минимуму (см. Ниже).

3. just a matter of preference

Нет, если вы хотите сохранить это асинхронным. Вы должны сделать это, чтобы сохранить асинхронные вещи в стеке.

Теперь, вы можете сделать, чтобы улучшить perf. Вот. Если вы просто обертываете асинхронный метод, вам не нужно использовать языковые функции - просто верните 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 будет захватывать контекст пользовательского интерфейса. "Внутренние" работы библиотеки обычно не касаются контекста синхронизации, поэтому лучше не записывать это.

Наконец, я бы предостерег вас от одного из ваших примеров:

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

Если вы создаете метод async, который внутри использует Task.Run для "создания асинхронности" вокруг чего-то, что не является асинхронным, вы эффективно завершаете синхронный код в метод async. Это будет использовать поток ThreadPool, но может "скрыть" тот факт, что он делает это, что делает API неверным. Часто бывает лучше оставить вызов Task.Run для вызовов самого высокого уровня и позволить основным методам оставаться синхронными, если они действительно не могут воспользоваться асинхронным IO или некоторыми средствами разгрузки, кроме Task.Run. (Это не всегда так, но код "асинхронный", перекодированный через синхронный код через Task.Run, затем возвращается через async/await, часто является признаком ошибочного дизайна.)