Должны ли методы вызова асинхронной библиотеки С# ждать?

Если метод асинхронной библиотеки вызывает await? Например, предположим, что у меня есть метод библиотеки служб данных, который имеет доступ к контексту данных Entity Framework 6 с именем "репозиторий". Насколько я могу судить, у меня есть два способа определения этого метода:

public static async Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    return await db.Blogs
        .OrderByDescending(b => b.Date)
        .SelectAsync();
}

или без async/await украшения

public static Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    return db.Blogs
        .OrderByDescending(b => b.Date)
        .SelectAsync();
}

В конечной точке приложения, в этом случае действие контроллера MVC, вызов будет одинаковым для любого метода:

public async Task<ActionResult> Blogs()
{
    var blogs = await BlogService.GetAllBlogs(_blogRepository);
    return View(blogs);
}

Этот сценарий, конечно, может быть более сложным, если приложение вызывает цепочку асинхронных методов. Если каждый метод в цепочке вызывает await, или должен быть только один оператор await в конце цепочки вызовов и какая разница это сделает?

Ответ 1

Ожидается ли вызов метода асинхронной библиотеки?

Короткий ответ: Да.

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

Удаление async и await является оптимизацией, но очень маленькой. И, как и большинство оптимизаций, это может вызвать проблемы, если вы не очень осторожны.

В вашем примере:

return db.Blogs
    .OrderByDescending(b => b.Date)
    .SelectAsync();

Это примерно то же самое, что:

var blogs = db.Blogs;
var query = blogs.OrderByDescending(b => b.Date);
var task = query.SelectAsync();

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

Ответ 2

Вы должны быть осторожны при переносе одноразового объекта на метод async.

using (var db = new EfDataContext())
{
    return BlogService.GetAllBlogs(db);
}

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

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

Edit:

В то время как это наиболее часто встречается с одноразовыми объектами внутри используемого блока, то же самое относится ко всем типам ссылок, где вы изменяете состояние объекта до завершения Задания.

Например, следующий пример будет вызывать DivideByZeroException после вызова await TestAsync();

public class MyClass
{
    public int Value { get; set; }
}

public static async Task<int> DivideAsync(MyClass myClass)
{
    await Task.Yield();
    return 2 / myClass.Value;
}

public static Task<int> TestAsync()
{
    MyClass myClass = new MyClass { Value = 4 };
    Task<int> result = DivideAsync(myClass);
    myClass.Value = 0;
    return result;
}

Итак, имхо лучше испытать задачу, если вы когда-либо изменяете состояние объекта, который будет использовать Task.

Ответ 3

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

Например, вы можете обернуть все исключения специальным типом для вашей библиотеки:

public static async Task<IEnumerable<Blogs>>
    GetAllBlogsAsync(EfDataContext db)
{
    try
    {
        // If a fault occurs in the linq query, the 
        // exception is raised by the 'await' statement
        return await db.Blogs
            .OrderByDescending(b => b.Date)
            .SelectAsync();
    }
    catch (Exception ex)
    {
        throw new BlogLibraryException("This blog appears to be broken.", ex);
    }
}

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

Пока вы не вызываете Task.Wait, Task.GetResult или Task.Result, вам не грозит блокировка. Накладные расходы на выполнение задачи незначительны, особенно если вы просто делегируете базу данных для "истинного async" (IO по границе системы).

Ответ 4

Я бы не использовал async там.

Я бы использовал его в библиотеке.

Если вы вызываете хвост методом, возвращающим задачу, то не используя async:

  • Быстрее.

  • Simpler.

  • В-linable.

  • Так же легко отлаживать (вас действительно смущает тот факт, что трассировка стека не получается из точки await?)

  • Вероятно, не будет исключения.

  • Неважно, исключение выбрано из SelectAsync, а не GetAllBlogsAsync

  • Тривиально заменить форму async, если что-то изменится, что делает ее более желательной.

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

Накладные расходы async низки, но зачем писать код, чтобы ввести некоторые накладные расходы?

Это не значит, что я бы ответил "Должны ли вызовы Asynchronous Library? с" нет". В тех случаях, когда async делает что-то другое, кроме того, что он вызывает метод возврата задачи, я бы это сделал. Я почти всегда делал это с помощью .ConfigureAwait(false), хотя, как правило, я не хочу, чтобы контекст из вызывающего кода передавался в ожидаемый я, и это может потенциально вызвать взаимоблокировки.

Ответ 5

Я бы следил за руководством Microsoft. Они не используют async для асинхронных методов библиотеки.

Stream.WriteAsync()

DbContext.SaveChangesAsync()

В отсутствие надлежащей документации от Microsoft, по-видимому, безопасно делать то, что на самом деле делает Microsoft. Я бы с удовольствием удалил свой ответ, если бы кто-то предоставил ссылку на страницу Microsoft, на которой обсуждалась проблема.