Дизайн с async/await - должно ли быть все async?

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

public void DoSomething(User user) 
{
    if (user.Gold > 1000) ChatManager.Send(user, "You are rich: " + user.Gold);
}

Через некоторое время я понимаю, что хочу изменить его:

public async Task DoSomething(User user) 
{
    if (user.Gold > 1000) ChatManager.Send(user, "You are rich: " + user.Gold);
    if (!user.HasReward)
    {
         using(var dbConnection = await DbPool.OpenConnectionAsync())
         {
             await dbConnection.Update(user, u => 
                        {
                            u.HasReward = true;
                            u.Gold += 1000;
                        });
         }
    }
}

Я меняю сигнатуру метода в интерфейсе. Но вызывающие методы были синхронными, и я должен сделать не только их async, но и все дерево вызовов async.

Пример:

void A()
{
    _peer.SendResponse("Ping: " + _x.B());
}

double X.B()
{
    return _someCollection.Where(item => y.C(item)).Average();
}


bool Y.C(int item)
{
   // ...
   _z.DoSomething();
   return _state.IsCorrect;
}

следует изменить на

async void AAsync()
{
    _peer.SendResponse("Ping: " + await _x.BAsync());
}

async Task<double> X.BAsync()
{
    // await thing breaks LINQ!
    var els = new List<int>();
    foreach (var el in _someCollection)
    {
        if (await y.CAsync(item)) els.Add(el);
    }
    return _els.Average();
}


async Task<bool> Y.CAsync(int item)
{
   // ...
   await _z.DoSomething();
   return _state.IsCorrect;
}

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

Также, когда первый метод A вызывается из метода интерфейса, такого как IDisposable.Dispose - я не могу сделать его асинхронным.

Другой пример: представьте, что несколько вызовов на A хранились в качестве делегатов. Раньше они вызывались только с _combinedDelegate.Invoke(), но теперь я должен пройти через GetInvocationList() и await для каждого элемента.

О, и рассмотрим также замену getter get методом async.

Я не могу использовать Task.Wait() или .Result, потому что:

  • Он тратит ThreadPool потоки в серверном приложении
  • Это приводит к взаимоблокировкам: если все ThreadPool потоки Wait ing, для выполнения каких-либо задач нет потоков.

Итак, вопрос в том, должен ли я сначала исправить все мои методы async, даже если я не планирую вызывать что-либо асинхронное внутри? Разве это не повредит работе? Или как проектировать вещи, чтобы избежать таких сложных рефакторингов?

Ответ 1

Должен ли я сделать абсолютно все мои методы изначально асинхронными, даже если я не планирую называть что-либо асинхронным внутри?

Эта проблема с конструкцией async по существу такая же, как проблема с IDisposable. То есть интерфейсы должны предсказывать их реализации. И это будет беспорядочно, независимо от того, что вы делаете.

По моему опыту, довольно просто рассмотреть метод/интерфейс/класс и предсказать, будет ли он использовать I/O или нет. Если для этого требуется ввод-вывод, то, вероятно, это должно быть сделано с возвратом задачи. Иногда (но не всегда) можно структурировать свой код, чтобы операции ввода-вывода выполнялись в его собственном разделе кода, оставив бизнес-объекты и логику строго синхронными. Хорошим примером этого является шаблон Redux в мире JavaScript.

Но нижняя строка, иногда вы делаете неправильный вызов и имеете рефакторинг. Я думаю, что это гораздо лучший подход, чем просто сделать каждый метод асинхронным. Вы наследуете каждый интерфейс от IDisposable и используете using везде? Нет, ты добавляешь только когда это необходимо; и вы должны использовать тот же подход с async.

Ответ 2

Должен ли я сделать абсолютно все мои методы изначально асинхронными, даже если я не планирую вызывать что-либо асинхронное внутри?

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

Как создавать вещи, чтобы избежать таких сложных рефакторингов?

В какой-то степени такие рефакторинги могут быть необходимы.

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