¿Las pautas de uso asincrónico / en espera de C # no están en contradicción con los conceptos de buena arquitectura y capas de abstracción?

101

Esta pregunta se refiere al lenguaje C #, pero espero que cubra otros lenguajes como Java o TypeScript.

Microsoft recomienda mejores prácticas sobre el uso de llamadas asíncronas en la red. Entre estas recomendaciones, escojamos dos:

  • cambie la firma de los métodos asíncronos para que devuelvan Tarea o Tarea < > (en TypeScript, sería una promesa < >)
  • cambie los nombres de los métodos asíncronos para terminar con xxxAsync ()

Ahora, cuando se reemplaza un componente síncrono de bajo nivel por uno asíncrono, esto afecta la pila completa de la aplicación. Dado que async / await tiene un impacto positivo solo si se usa "hasta el final", significa que se deben cambiar los nombres de la firma y el método de cada capa en la aplicación.

Una buena arquitectura a menudo implica colocar abstracciones entre cada capa, de manera que la sustitución de componentes de bajo nivel por otras no se ve por los componentes de nivel superior. En C #, las abstracciones toman la forma de interfaces. Si introducimos un nuevo componente asíncrono de bajo nivel, cada interfaz de la pila de llamadas debe modificarse o reemplazarse por una nueva interfaz. La forma en que se resuelve un problema (asíncrono o sincronización) en una clase de implementación ya no está oculta (abstraída) para los llamadores. Las personas que llaman deben saber si es sincronización o asíncrona.

¿Las prácticas recomendadas no son asincrónicas / a la espera de los principios de "buena arquitectura"?

¿Significa que cada interfaz (por ejemplo, IEnumerable, IDataAccessLayer) necesita su contraparte asíncrona (IAsyncEnumerable, IAsyncDataAccessLayer) para que pueda ser reemplazada en la pila cuando se cambia a las dependencias async?

Si presionamos el problema un poco más, ¿no sería más sencillo asumir que todos los métodos son asíncronos (para devolver una Tarea < > o Promesa < >), y los métodos para sincronizar las llamadas asíncronas cuando ¿No son en realidad asíncronos? ¿Es esto algo que se puede esperar de los futuros lenguajes de programación?

    
pregunta corentinaltepe 05.12.2018 - 09:22
fuente

5 respuestas

108

¿De qué color es tu función?

Puede que estés interesado en de Bob Nystrom de qué color es tu función 1 .

En este artículo, describe un lenguaje ficticio donde:

  • Cada función tiene un color: azul o rojo.
  • Una función roja puede llamar a funciones azules o rojas, sin problemas.
  • Una función azul solo puede llamar a funciones azules.

Aunque es ficticio, esto sucede con bastante frecuencia en los lenguajes de programación:

  • En C ++, un método "const" solo puede llamar a otros métodos "const" en this .
  • En Haskell, una función que no sea IO solo puede llamar a funciones que no sean IO.
  • En C #, una función de sincronización solo puede llamar a las funciones de sincronización 2 .

Como se ha dado cuenta, debido a estas reglas, las funciones rojas tienden a extenderse alrededor de la base del código. Usted inserta uno, y poco a poco coloniza toda la base de código.

1 Bob Nystrom, aparte de los blogs, también es parte del equipo de Dart y ha escrito esta pequeña serie de Intérpretes de artesanía; Muy recomendable para cualquier lenguaje de programación / aficionado al compilador.

2 No es del todo cierto, ya que puede llamar a una función asíncrona y bloquear hasta que regrese, pero ...

Limitación de idioma

Esto es, esencialmente, una limitación de idioma / tiempo de ejecución.

El lenguaje con subprocesos M: N, por ejemplo, como Erlang y Go, no tiene funciones async : cada función es potencialmente asíncrona y su "fibra" simplemente se suspenderá, intercambiará e intercambiará de nuevo cuando está listo de nuevo.

C # se fue con un modelo de subprocesos 1: 1 y, por lo tanto, decidió destacar la sincronicidad en el lenguaje para evitar el bloqueo accidental de subprocesos.

En presencia de limitaciones de idioma, las directrices de codificación deben adaptarse.

    
respondido por el Matthieu M. 05.12.2018 - 13:58
fuente
81

Tienes razón, aquí hay una contradicción, pero no es que las "mejores prácticas" sean malas. Es porque la función asíncrona hace algo esencialmente diferente que una síncrona. En lugar de esperar el resultado de sus dependencias (por lo general, algunas IO), crea una tarea a ser manejada por el bucle de eventos principal. Esta no es una diferencia que pueda ocultarse bien en la abstracción.

    
respondido por el max630 05.12.2018 - 10:58
fuente
6

Un método asíncrono se comporta de manera diferente a uno que es sincrónico, como estoy seguro. En tiempo de ejecución, convertir una llamada asíncrona a una síncrona es trivial, pero no se puede decir lo contrario. Entonces, por lo tanto, la lógica se convierte en: ¿por qué no hacemos métodos asíncronos de todos los métodos que lo requieran y dejamos que la persona que llama "convierta" según sea necesario a un método sincrónico?

En cierto sentido, es como tener un método que lanza excepciones y otro que es "seguro" y no se produce incluso en caso de error. ¿En qué momento es excesivo el codificador para proporcionar estos métodos que, de lo contrario, pueden convertirse uno en otro?

En esto hay dos escuelas de pensamiento: una es crear múltiples métodos, cada uno de los cuales llama a otro método posiblemente privado, lo que permite la posibilidad de proporcionar parámetros opcionales o alteraciones menores del comportamiento, como ser asíncrono. La otra es minimizar los métodos de interfaz a lo esencial, dejando que la persona que llama realice las modificaciones necesarias por sí mismo.

Si eres de la primera escuela, hay una cierta lógica para dedicar una clase a las llamadas síncronas y asíncronas para evitar duplicar cada llamada. Microsoft tiende a favorecer esta escuela de pensamiento, y por convención, para mantener su coherencia con el estilo preferido por Microsoft, usted también tendría que tener una versión Async, de la misma manera que las interfaces casi siempre comienzan con una "I". Permítame enfatizar que no es incorrecto , per se, porque es mejor mantener un estilo consistente en un proyecto en lugar de hacerlo "de la manera correcta" y cambiar radicalmente el estilo para el desarrollo que usted desarrolla. añadir a un proyecto.

Dicho esto, tiendo a favorecer la segunda escuela, que es minimizar los métodos de interfaz. Si creo que un método puede llamarse de forma asíncrona, el método para mí es asíncrono. La persona que llama puede decidir si esperar o no a que la tarea termine antes de continuar. Si esta interfaz es una interfaz para una biblioteca, es más razonable hacerlo de esta manera para minimizar la cantidad de métodos que necesitaría para desaprobar o ajustar. Si la interfaz es para uso interno en mi proyecto, agregaré un método para cada llamada necesaria a lo largo de mi proyecto para los parámetros proporcionados y sin métodos "adicionales", e incluso entonces, solo si el comportamiento del método aún no está cubierto por un método existente.

Sin embargo, como muchas cosas en este campo, es en gran medida subjetiva. Ambos enfoques tienen sus pros y sus contras. Microsoft también comenzó la convención de agregar letras indicativas de tipo al comienzo del nombre de la variable y "m_" para indicar que es un miembro, lo que lleva a nombres de variables como m_pUser . Mi punto es que ni siquiera Microsoft es infalible, y también puede cometer errores.

Dicho esto, si su proyecto está siguiendo esta convención de Async, le aconsejaría que lo respete y continúe con el estilo. Y solo una vez que te dan un proyecto propio, puedes escribirlo de la mejor manera que creas conveniente.

    
respondido por el Neil 05.12.2018 - 10:16
fuente
2

Imaginemos que hay una manera de permitirle llamar a funciones de forma asíncrona sin cambiar su firma.

Eso sería realmente genial y nadie recomendaría que cambies sus nombres.

Pero, las funciones asíncronas reales, no solo las que esperan a otra función asíncrona, sino que el nivel más bajo tienen alguna estructura específica para su naturaleza asíncrona. por ejemplo,

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

Son esos dos bits de información que necesita el código de llamada para ejecutar el código de forma asíncrona que cosas como Task y async encapsulan.

Hay más de una forma de realizar funciones asíncronas, async Task es solo un patrón integrado en el compilador a través de los tipos de retorno para que no tenga que vincular manualmente los eventos

    
respondido por el Ewan 05.12.2018 - 12:16
fuente
0

Abordaré el punto principal de una manera menos precisa y más genérica:

  

¿Las prácticas recomendadas no son asincrónicas / a la espera de los principios de "buena arquitectura"?

Yo diría que solo depende de la elección que hagas en el diseño de tu API y de lo que le permitas al usuario.

Si desea que una función de su API sea solo asíncrona, hay poco interés en seguir la convención de nomenclatura. Simplemente siempre devuelva la Tarea < > / Promesa < > / Futuro < > / ... como tipo de devolución, se auto-documenta. Si quiere una respuesta sincronizada, aún podrá hacerlo esperando, pero si siempre lo hace, creará un poco de repetición.

Sin embargo, si solo realiza la sincronización de su API, eso significa que si un usuario desea que sea asíncrono, tendrá que administrar la parte asíncrona de él mismo.

Esto puede hacer un montón de trabajo adicional, sin embargo, también puede dar más control al usuario sobre la cantidad de llamadas simultáneas que permite, el tiempo de espera, el retry, etc., etc.

En un sistema grande con una API enorme, implementar la mayoría de ellos para que se sincronicen de manera predeterminada puede ser más fácil y más eficiente que administrar de manera independiente cada parte de su API, especialmente si comparten recursos (sistema de archivos, CPU, base de datos, ... .).

De hecho, para las partes más complejas, podría perfectamente tener dos implementaciones de la misma parte de su API, una sincrónica haciendo las cosas prácticas, una asincrónica que depende de la sincrónica que maneja las cosas y solo gestionando la concurrencia, las cargas, los tiempos de espera y reintentos.

Tal vez alguien más pueda compartir su experiencia con eso porque no tengo experiencia con tales sistemas.

    
respondido por el Walfrat 05.12.2018 - 16:44
fuente

Lea otras preguntas en las etiquetas