¿Cómo evitar violar el principio DRY cuando tiene que tener versiones de código asíncronas y sincronizadas?

15

Estoy trabajando en un proyecto que necesita admitir versiones asíncronas y sincronizadas de una misma lógica / método. Entonces, por ejemplo, necesito tener:

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

La lógica asíncrona y de sincronización para estos métodos es idéntica en todos los aspectos, excepto que uno es asíncrono y el otro no. ¿Hay alguna forma legítima de evitar violar el principio DRY en este tipo de escenario? Vi que la gente dice que puedes usar GetAwaiter (). GetResult () en un método asíncrono e invocarlo desde tu método de sincronización. ¿Es seguro ese hilo en todos los escenarios? ¿Hay otra forma mejor de hacer esto o me veo obligado a duplicar la lógica?

Marko
fuente
1
¿Puedes decir un poco más sobre lo que estás haciendo aquí? Específicamente, ¿cómo está logrando la asincronía en su método asincrónico? ¿El trabajo de alta latencia está vinculado a la CPU o IO?
Eric Lippert
"uno es asíncrono y otro no" ¿es la diferencia en el código del método real o solo en la interfaz? (Claramente por la interfaz que puedes return Task.FromResult(IsIt());)
Alexei Levenkov
Además, presumiblemente las versiones síncronas y asíncronas son de alta latencia. ¿En qué circunstancias la persona que llama utilizará la versión síncrona, y cómo va a mitigar el hecho de que la persona que llama podría tomar miles de millones de nanosegundos? ¿A la persona que llama de la versión sincrónica no le importa que cuelguen la interfaz de usuario? ¿No hay interfaz de usuario? Cuéntanos más sobre el escenario.
Eric Lippert
@EricLippert Agregué un ejemplo ficticio específico solo para darle una idea de cómo las bases de 2 códigos son idénticas además del hecho de que una es asíncrona. Puede imaginar fácilmente un escenario en el que este método es mucho más complejo y las líneas y líneas de código deben duplicarse.
Marko
@AlexeiLevenkov La diferencia está en el código, agregué un código ficticio para demostrarlo.
Marko

Respuestas:

15

Hiciste varias preguntas en tu pregunta. Los desglosaré de manera ligeramente diferente a como lo hiciste tú. Pero primero déjame responder directamente a la pregunta.

Todos queremos una cámara que sea liviana, de alta calidad y barata, pero como dice el dicho, solo puede obtener como máximo dos de esos tres. Estás en la misma situación aquí. Desea una solución que sea eficiente, segura y que comparta código entre las rutas síncronas y asíncronas. Solo obtendrás dos de esos.

Déjame desglosar por qué es eso. Comenzaremos con esta pregunta:


¿Vi que la gente dice que puedes usar GetAwaiter().GetResult()un método asíncrono e invocarlo desde tu método de sincronización? ¿Es seguro ese hilo en todos los escenarios?

El punto de esta pregunta es "¿puedo compartir las rutas sincrónicas y asincrónicas haciendo que la ruta sincrónica simplemente haga una espera sincrónica en la versión asincrónica?"

Permítanme ser muy claro en este punto porque es importante:

DEBES DEJAR DE INMEDIATO TOMAR CUALQUIER CONSEJO DE AQUELLAS PERSONAS .

Ese es un consejo extremadamente malo. Es muy peligroso obtener sincrónicamente un resultado de una tarea asincrónica a menos que tenga evidencia de que la tarea se ha completado normal o anormalmente .

La razón por la que este es un consejo extremadamente malo es, bueno, considere este escenario. Desea cortar el césped, pero la cuchilla del cortacésped está rota. Decide seguir este flujo de trabajo:

  • Solicite una nueva cuchilla de un sitio web. Esta es una operación asíncrona de alta latencia.
  • Sincrónicamente espere, es decir, duerma hasta que tenga la cuchilla en la mano .
  • Revise periódicamente el buzón para ver si ha llegado la hoja.
  • Retire la cuchilla de la caja. Ahora lo tienes en la mano.
  • Instale la cuchilla en el cortacésped.
  • Cortar el césped.

¿Lo que pasa? Duermes para siempre porque la operación de revisar el correo ahora está cerrada en algo que sucede después de que llega el correo .

Es extremadamente fácil entrar en esta situación cuando espera sincrónicamente una tarea arbitraria. Esa tarea podría tener un trabajo programado en el futuro del hilo que ahora está esperando , y ahora ese futuro nunca llegará porque lo estás esperando.

Si haces una espera asincrónica, ¡ entonces todo está bien! Revisas periódicamente el correo y, mientras esperas, haces un sándwich o haces tus impuestos o lo que sea; sigues haciendo el trabajo mientras esperas.

Nunca sincrónicamente espere. Si la tarea está hecha, es innecesaria . Si la tarea no se realiza pero está programada para ejecutarse en el subproceso actual, es ineficiente porque el subproceso actual podría estar sirviendo a otro trabajo en lugar de esperar. Si la tarea no se realiza y la ejecución programada en el subproceso actual, se bloquea para esperar sincrónicamente. No hay una buena razón para esperar sincrónicamente, de nuevo, a menos que ya sepa que la tarea está completa .

Para leer más sobre este tema, vea

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

Stephen explica el escenario del mundo real mucho mejor que yo.


Ahora consideremos la "otra dirección". ¿Podemos compartir código haciendo que la versión asincrónica simplemente haga la versión sincrónica en un subproceso de trabajo?

Es posible y, de hecho, probablemente una mala idea, por las siguientes razones.

  • Es ineficiente si la operación síncrona es un trabajo de E / S de alta latencia. Esto esencialmente contrata a un trabajador y lo hace dormir hasta que se realiza una tarea. Los hilos son increíblemente caros . Consumen un millón de bytes de espacio mínimo de direcciones por defecto, toman tiempo, toman recursos del sistema operativo; no quieres quemar un hilo haciendo un trabajo inútil.

  • La operación síncrona podría no estar escrita para ser segura para subprocesos.

  • Esta es una técnica más razonable si el trabajo de alta latencia está vinculado al procesador, pero si es así, probablemente no desee simplemente pasarlo a un subproceso de trabajo. Es probable que desee utilizar la biblioteca paralela de tareas para paralelizarla a tantas CPU como sea posible, probablemente desee la lógica de cancelación y no puede simplemente hacer que la versión síncrona haga todo eso, porque entonces ya sería la versión asíncrona .

Otras lecturas; nuevamente, Stephen lo explica muy claramente:

Por qué no usar Task.Run:

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

Más escenarios de "hacer y no hacer" para Task.Run:

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


¿Con qué nos deja eso? Ambas técnicas para compartir código conducen a puntos muertos o grandes ineficiencias. La conclusión a la que llegamos es que debe tomar una decisión. ¿Desea un programa que sea eficiente y correcto y que deleite a la persona que llama, o desea guardar algunas pulsaciones de teclas implicadas al duplicar una pequeña cantidad de código entre las rutas síncronas y asíncronas? No entiendes las dos, me temo.

Eric Lippert
fuente
¿Puede explicar por qué regresar Task.Run(() => SynchronousMethod())en la versión asíncrona no es lo que quiere el OP?
Guillaume Sasdy
2
@GuillaumeSasdy: El póster original ha respondido a la pregunta de cuál es el trabajo: está sujeto a IO. ¡Es un desperdicio ejecutar tareas vinculadas a IO en un hilo de trabajo!
Eric Lippert
Muy bien lo entiendo. Creo que tienes razón, y también la respuesta de @StriplingWarrior lo explica aún más.
Guillaume Sasdy
2
@Marko casi cualquier solución alternativa que bloquee el hilo eventualmente (bajo alta carga) consumirá todos los hilos del conjunto de hilos (que generalmente se usa para completar las operaciones asíncronas). Como resultado, todos los subprocesos estarán esperando y ninguno estará disponible para permitir que las operaciones ejecuten "parte de la operación asíncrona completa" del código. La mayoría de las soluciones están bien en escenarios de baja carga (ya que hay muchos subprocesos, incluso 2-3 están bloqueados como parte de cada operación individual) ... Y si puede garantizar que la ejecución de la operación asincrónica se ejecuta en un nuevo subproceso del sistema operativo (no grupo de subprocesos) incluso puede funcionar para todos los casos (usted paga un alto precio)
Alexei Levenkov
2
A veces, realmente no hay otra manera que "simplemente hacer la versión sincrónica en un subproceso de trabajo". Por ejemplo, así Dns.GetHostEntryAsynces como se implementa en .NET, y FileStream.ReadAsyncpara ciertos tipos de archivos. El sistema operativo simplemente no proporciona una interfaz asíncrona, por lo que el tiempo de ejecución tiene que fingirlo (y no es específico del idioma; por ejemplo, el tiempo de ejecución de Erlang ejecuta todo el árbol de procesos de trabajo , con múltiples hilos dentro de cada uno, para proporcionar E / S de disco sin bloqueo y nombre resolución).
Joker_vD
6

Es difícil dar una respuesta única para todos. Desafortunadamente, no hay una manera simple y perfecta de reutilizar el código asíncrono y síncrono. Pero aquí hay algunos principios a considerar:

  1. El código asíncrono y síncrono a menudo es fundamentalmente diferente. El código asincrónico generalmente debe incluir un token de cancelación, por ejemplo. Y a menudo termina llamando a diferentes métodos (como su ejemplo llama Query()en uno y QueryAsync()en el otro), o configurando conexiones con diferentes configuraciones. Entonces, incluso cuando es estructuralmente similar, a menudo hay suficientes diferencias de comportamiento para merecer que se traten como un código separado con diferentes requisitos. Tenga en cuenta las diferencias entre implementaciones de métodos Async y Sync en la clase File, por ejemplo: no se hace ningún esfuerzo para que usen el mismo código
  2. Si proporciona una firma de método asincrónico por el simple hecho de implementar una interfaz, pero resulta que tiene una implementación sincrónica (es decir, no hay nada inherentemente asíncrono sobre lo que hace su método), simplemente puede regresar Task.FromResult(...).
  3. Cualquier pieza síncrona de lógica que sea igual entre los dos métodos puede extraerse en un método auxiliar separado y aprovecharse en ambos métodos.

Buena suerte.

StriplingWarrior
fuente
-2

Fácil; haga que el síncrono llame al asíncrono. Incluso hay un método útil Task<T>para hacer exactamente eso:

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
fuente
1
Vea mi respuesta sobre por qué esta es una peor práctica que nunca debe hacer.
Eric Lippert
1
Su respuesta trata con GetAwaiter (). GetResult (). Yo evité eso. La realidad es que a veces hay que hacer que un método se ejecute sincrónicamente para usarlo en una pila de llamadas que no se puede hacer asíncrono. Si las esperas de sincronización nunca se deben hacer, entonces, como miembro (anterior) del equipo de C #, dígame por qué no puso una sino dos formas de esperar sincrónicamente una tarea asincrónica en el TPL.
KeithS
La persona que haría esa pregunta sería Stephen Toub, no yo. Solo trabajé en el lado del lenguaje de las cosas.
Eric Lippert