Как избежать нарушения принципа СУХОЙ, когда вам нужны как асинхронные, так и синхронизированные версии кода?

15

Я работаю над проектом, который должен поддерживать как асинхронную, так и синхронизированную версию одной и той же логики / метода. Так, например, мне нужно иметь:

public class Foo
{
   public bool IsIt()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return conn.Query<bool>("SELECT IsIt FROM SomeTable");
      }
   }

   public async Task<bool> IsItAsync()
   {
      using (var conn = new SqlConnection(DB.ConnString))
      {
         return await conn.QueryAsync<bool>("SELECT IsIt FROM SomeTable");
      }
   }
}

Логика асинхронности и синхронизации для этих методов идентична во всех отношениях, за исключением того, что один является асинхронным, а другой - нет. Есть ли законный способ избежать нарушения принципа СУХОЙ в таком сценарии? Я видел, что люди говорят, что вы можете использовать GetAwaiter (). GetResult () для асинхронного метода и вызывать его из вашего метода синхронизации? Этот поток безопасен во всех сценариях? Есть ли другой, лучший способ сделать это, или я вынужден дублировать логику?

Marko
источник
1
Не могли бы вы рассказать немного больше о том, что вы здесь делаете? В частности, как вы достигаете асинхронности в вашем асинхронном методе? Связан ли высокопроизводительный рабочий процессор или IO?
Эрик Липперт
«Один асинхронный, а другой нет» - это разница в коде реального метода или просто в интерфейсе? (Понятно, что просто интерфейс можно return Task.FromResult(IsIt());)
Алексей Левенков
Кроме того, предположительно, синхронная и асинхронная версии имеют высокую задержку. При каких обстоятельствах вызывающий абонент будет использовать синхронную версию, и как этот вызывающий абонент будет смягчать тот факт, что вызываемый абонент может принимать миллиарды наносекунд? Разве вызывающей стороне синхронной версии не важно, что они вешают интерфейс? Там нет интерфейса? Расскажите нам больше о сценарии.
Эрик Липперт
@EricLippert Я добавил специальный фиктивный пример, чтобы дать вам представление о том, как две базы кодов идентичны, кроме того факта, что одна является асинхронной. Вы можете легко представить сценарий, в котором этот метод намного сложнее и строки и строки кода должны быть продублированы.
Марко
@AlexeiLevenkov Разница действительно в коде, я добавил несколько фиктивных кодов для демонстрации.
Марко

Ответы:

15

Вы задали несколько вопросов в своем вопросе. Я сломаю их немного иначе, чем ты. Но сначала позвольте мне прямо ответить на вопрос.

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

Позвольте мне объяснить, почему это так. Начнем с этого вопроса:


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

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

Позвольте мне быть предельно ясным по этому вопросу, потому что это важно:

ВЫ ДОЛЖНЫ НЕМЕДЛЕННО ОСТАНОВИТЬСЯ, ЧТОБЫ СОВЕТАТЬСЯ С ЭТИМИ ЛЮДЯМИ .

Это очень плохой совет. Очень опасно синхронно получать результат от асинхронной задачи, если у вас нет доказательств того, что задача выполнена нормально или ненормально .

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

  • Закажите новый клинок с веб-сайта. Это асинхронная операция с высокой задержкой.
  • Синхронно ждите, то есть спите, пока у вас не будет лезвия в руке .
  • Периодически проверяйте почтовый ящик, чтобы увидеть, прибыл ли блейд.
  • Выньте лезвие из коробки. Теперь у вас есть это в руках.
  • Установите лезвие в косилку.
  • Косить газон.

Что просходит? Вы спите вечно, потому что операция проверки почты теперь зависит от того, что происходит после получения почты .

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

Если вы делаете асинхронное ожидание, тогда все в порядке! Вы периодически проверяете почту, и пока вы ждете, вы делаете бутерброд или платите налоги или что-то еще; Вы продолжаете работать, пока ждете.

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

Для дальнейшего чтения по этой теме, см.

https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

Стивен объясняет сценарий реального мира гораздо лучше, чем я.


Теперь давайте рассмотрим «другое направление». Можем ли мы поделиться кодом, сделав асинхронную версию просто выполняющей синхронную версию в рабочем потоке?

То есть , возможно , и в самом деле , вероятно , плохая идея, по следующим причинам.

  • Это неэффективно, если синхронная операция ввода-вывода с высокой задержкой. По сути, это нанимает работника и заставляет его спать до тех пор, пока задача не будет выполнена. Нитки безумно дороги . По умолчанию они потребляют минимум миллион байтов адресного пространства, им требуется время, они берут ресурсы операционной системы; Вы не хотите прожечь нить, делая бесполезную работу.

  • Синхронная операция может быть записана не как потокобезопасная.

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

Дальнейшее чтение; опять же, Стивен объясняет это очень четко:

Почему бы не использовать Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

Больше сценариев «делай и не делай» для Task.Run:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-dont-use.html


Что это тогда оставляет нас с? Обе технологии для совместного использования кода приводят либо к тупикам, либо к большой неэффективности. Мы пришли к выводу, что вы должны сделать выбор. Вам нужна программа, которая является эффективной и правильной и доставляет удовольствие вызывающей стороне, или вы хотите сохранить несколько нажатий клавиш, дублируя небольшой объем кода между синхронным и асинхронным путями? Боюсь, вы не получите и то, и другое.

Эрик Липперт
источник
Можете ли вы объяснить, почему возвращение Task.Run(() => SynchronousMethod())в асинхронной версии не то, что хочет ОП?
Гийом Сасди
2
@GuillaumeSasdy: Оригинальный постер ответил на вопрос о том, что это за работа: она связана с IO. Бесполезно запускать связанные с IO задачи в рабочем потоке!
Эрик Липперт
Хорошо, я понимаю. Я думаю, что вы правы, и ответ @StriplingWarrior объясняет это еще больше.
Гийом Сасди
2
@Marko почти любой обходной путь, который блокирует поток, в конечном итоге (при высокой нагрузке) потребляет все потоки пула потоков (которые обычно используются для выполнения асинхронных операций). В результате все потоки будут ожидать, и ни один из них не будет доступен, чтобы позволить операциям выполнить часть кода «асинхронная операция завершена». Большинство обходных путей хороши в сценариях с низкой нагрузкой (поскольку существует множество потоков, даже 2-3 блокируются как часть каждой отдельной операции)… И если вы можете гарантировать, что завершение асинхронной операции выполняется в новом потоке ОС (не в пуле потоков), может даже работать на все случаи (так вы платите высокую цену)
Алексей Левенков
2
Иногда нет другого способа, кроме как «просто сделать синхронную версию в рабочем потоке». Например, вот как Dns.GetHostEntryAsyncэто реализовано в .NET и FileStream.ReadAsyncдля определенных типов файлов. ОС просто не предоставляет асинхронный интерфейс, поэтому среда выполнения должна имитировать его (и это не зависит от языка - скажем, среда выполнения Erlang запускает все дерево рабочих процессов с несколькими потоками внутри каждого, чтобы обеспечить неблокируемый дисковый ввод-вывод и имя разрешающая способность).
Joker_vD
6

Сложно дать единый ответ на этот вопрос. К сожалению, не существует простого, идеального способа повторного использования асинхронного и синхронного кода. Но вот несколько принципов для рассмотрения:

  1. Асинхронный и синхронный код часто принципиально отличается. Асинхронный код обычно должен включать токен отмены, например. И часто это заканчивает тем, что вызывает разные методы (как ваш пример вызывает Query()один и QueryAsync()другой) или устанавливает соединения с разными настройками. Таким образом, даже когда он структурно похож, часто бывает достаточно различий в поведении, чтобы его можно было рассматривать как отдельный код с различными требованиями. Обратите внимание на различия между реализациями методов Async и Sync в классе File, например: не предпринимается никаких усилий, чтобы заставить их использовать один и тот же код
  2. Если вы предоставляете подпись асинхронного метода для реализации интерфейса, но у вас есть синхронная реализация (т.е. нет ничего асинхронного в том, что делает ваш метод), вы можете просто вернуться Task.FromResult(...).
  3. Любые куски синхронных логики , которые представляют собой то же самое между этими двумя методами могут быть извлечены в отдельный вспомогательный метод и заемных средств в обоих методах.

Удачи.

StriplingWarrior
источник
-2

Легко; пусть синхронный вызов асинхронный. Есть даже удобный способ Task<T>сделать это:

public class Foo
{
   public bool IsIt()
   {
      var task = IsItAsync(); //no await here; we want the Task

      //Some tasks end up scheduled to run before you get them;
      //don't try to run them a second time
      if((int)task.Status > (int)TaskStatus.Created)
          //this call will block the current thread,
          //and unlike Run()/Wait() will prefer the current 
          //thread's TaskScheduler instead of a new thread.
          task.RunSynchronously(); 

      //if IsItAsync() can throw exceptions,
      //you still need a Wait() call to bring those back from the Task
      try{ 
          task.Wait();
          return task.Result;
      }
      catch(Exception ex) 
      { 
          //Handle IsItAsync() exceptions here;
          //remember to return something if you don't rethrow              
      }
   }

   public async Task<bool> IsItAsync()
   {
      // Some async logic
   }
}
Keiths
источник
1
Смотрите мой ответ, почему это худшая практика, которую вы никогда не должны делать.
Эрик Липперт
1
Ваш ответ касается GetAwaiter (). GetResult (). Я избегал этого. Реальность такова, что иногда вам приходится запускать метод синхронно, чтобы использовать его в стеке вызовов, который нельзя сделать асинхронным. Если ожидание синхронизации никогда не должно выполняться, как (бывший) член команды C #, скажите, пожалуйста, почему вы добавили не один, а два способа синхронного ожидания асинхронной задачи в TPL.
Кит
Человеком, который задаст этот вопрос, будет Стивен Тауб, а не я. Я работал только на языковой стороне вещей.
Эрик Липперт