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


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

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

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

это:

  1. необходимые
  2. отрицательно на производительности
  3. просто вопрос предпочтений

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

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

2 ответа:

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

этот вопрос предполагает несколько областей непонимания.

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

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

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

любой асинхронный метод, где у вас есть один 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();
}

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

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

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

Is это:

1. necessary

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

2. negative on performance

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

3. just a matter of preference

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

теперь, есть некоторые вещи, которые вы можете сделать, чтобы улучшить perf. здесь. Если вы просто обертываете асинхронный метод, вам не нужно использовать языковые функции - просто верните Task:

public virtual Task Save()
{
    return repository.Save();
}

The repository.Save() метод уже возвращает a 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(() => "");
}

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