Как избежать нарушения принципа DRY, когда вам нужны версии кода asyn c и syn c? - PullRequest
15 голосов
/ 08 января 2020

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

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");
      }
   }
}

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

Ответы [ 3 ]

15 голосов
/ 08 января 2020

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

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

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


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

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

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

ВЫ НЕМЕДЛЕННО ОСТАНОВИТЕСЬ, ЧТОБЫ СОВЕТАТЬ С НИХ ЛЮДЕЙ .

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

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

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

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

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

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

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

Подробнее о данной теме c см.

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

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


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

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

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

  • Возможно, синхронная операция не записана как поточно-ориентированная.

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

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

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

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

More "делай и не делай" "Scenar ios для Task.Run:

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


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

6 голосов
/ 08 января 2020

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

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

Удачи.

0 голосов
/ 08 января 2020

Легкий; пусть синхронный вызывает асин c один. На 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
   }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...