Как создавать и реализовывать интерфейсы для операций, которые только иногда бывают асинхронными

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

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

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

В настоящее время у меня есть только один метод интерфейса, который является асинхронным. Затем для синхронных реализаций они заключают код в объект Task:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText()
        {
            return Task.Run(() => "hello world");
        }
    }
}

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


person cat_in_hat    schedule 01.04.2019    source источник
comment
Я думаю, что у вас есть правильная идея, заставить интерфейс возвращать задачу, но ваша синхронная реализация неверна. Не используйте Task.Run(...), используйте Task.FromResult(...);. Ваша библиотека не должна использовать Task.Run(...), потому что это будет использовать новый поток (вроде как, это сложнее), а не просто работать в текущем выполняющемся потоке.   -  person Nelson    schedule 01.04.2019
comment
Как вы думаете, почему вам нужно иметь одно логическое свойство, чтобы указать вызывающей стороне, какой метод вызывать?   -  person Enigmativity    schedule 01.04.2019
comment
Мне не нужно логическое значение, это была одна из идей, чтобы избежать переноса кода внутри Task.   -  person cat_in_hat    schedule 01.04.2019
comment
Комментарий от @Nelson кажется лучшим ответом   -  person cat_in_hat    schedule 02.04.2019


Ответы (3)


Это обычная ситуация с интерфейсами. Если у вас есть контракт, в котором необходимо указать task для шаблона асинхронного ожидания, и мы должны реализовать этот Task в интерфейсе.

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

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

Общие способы использования

Стандарт async

public async Task<string> PullText()
{
   using (var reader = File.OpenText("Words.txt"))
   {
      return await reader.ReadToEndAsync();
   }
}

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

public Task<string> PullText()
{
   try
   {
      return Task.Run(() => DoCpuWork());
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

Чуть менее эффективен, так как мы чиним IAsyncStateMachine

public async Task<string> PullText()
{
    return await Task.Run(() => DoCpuWork());
}

Возврат завершенного Task с простыми результатами (захват исключения и помещение его в Task)

public Task<string> PullText()
{
   try
   {
      // simplified example
      return Task.FromResult("someString");
   }
   catch (Exception e)
   {
      return Task.FromException<string>(e);
   }
}

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

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.Run(() => "hello world");
}

а также

#pragma warning disable 1998
public async Task<string> PullText()()
#pragma warning restore 1998
{
    return Task.FromResult("someString");
}

Обратите внимание, все вышеперечисленное касается возврата Task<T> из метода. Если кто-то просто хотел вернуть Task, вы можете воспользоваться Task.CompletedTask; с той же семантикой ошибок, что и выше.

person TheGeneral    schedule 01.04.2019
comment
Согласен с этим ответом в целом, но не с использованием Task.Run. IMO Task.Run следует использовать только по мере необходимости для потребляющего кода, т. е. при вызове из потока пользовательского интерфейса, но не из потока пула потоков. - person Stephen Cleary; 01.04.2019
comment
Спасибо @StephenCleary, я купил вашу книгу! - person cat_in_hat; 23.10.2019

Обычно в этих случаях у вас есть что-то перед вызовом, который обрабатывает запрос и передает его «рабочим» классам (например, TestApp). Если это так, я не понимаю, почему наличие интерфейса «IAsyncable», в котором вы можете проверить, является ли класс асинхронным, не будет работать.

if(thisObject is IAscyncAble) {
  ... call the ansync request.
}
person alwayslearning    schedule 01.04.2019

В итоге я использовал следующий код:

using System.IO;
using System.Threading.Tasks;

namespace TestApp
{
    interface IBlackBox // interface for both sync and async execution
    {
        Task<string> PullText();
    }

    sealed class MyAsyncBlackBox : IBlackBox
    {
        public async Task<string> PullText()
        {
            using (var reader = File.OpenText("Words.txt"))
            {
                return await reader.ReadToEndAsync();
            }
        }
    }

    sealed class MyCachedBlackBox : IBlackBox
    {
        public Task<string> PullText() // notice no 'async' keyword
        {
            return Task.FromResult("hello world");
        }
    }
}
person cat_in_hat    schedule 02.04.2019