¿Cuál es la diferencia entre programación asincrónica y multihilo?

234

Pensé que eran básicamente lo mismo: escribir programas que dividen las tareas entre procesadores (en máquinas que tienen 2+ procesadores). Entonces estoy leyendo esto , que dice:

Los métodos asíncronos están destinados a ser operaciones sin bloqueo. Una expresión de espera en un método asíncrono no bloquea el hilo actual mientras se ejecuta la tarea esperada. En cambio, la expresión registra el resto del método como una continuación y devuelve el control al llamador del método asíncrono.

Las palabras clave asíncronas y en espera no hacen que se creen hilos adicionales. Los métodos asincrónicos no requieren subprocesos múltiples porque un método asincrónico no se ejecuta en su propio hilo. El método se ejecuta en el contexto de sincronización actual y usa el tiempo en el hilo solo cuando el método está activo. Puede usar Task.Run para mover el trabajo vinculado a la CPU a un subproceso en segundo plano, pero un subproceso en segundo plano no ayuda con un proceso que solo está esperando que los resultados estén disponibles.

y me pregunto si alguien puede traducir eso al inglés para mí. Parece establecer una distinción entre asincronicidad (¿es eso una palabra?) Y subprocesos e implica que puede tener un programa que tiene tareas asincrónicas pero no subprocesamiento múltiple.

Ahora entiendo la idea de tareas asincrónicas como el ejemplo en la pág. 467 de C # In Jon Deke Skeet en profundidad, tercera edición

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

La asyncpalabra clave significa " Esta función, siempre que se llame, no se llamará en un contexto en el que se requiera su finalización para todo después de que se llame su llamada".

En otras palabras, escribirlo en medio de alguna tarea

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, ya que DisplayWebsiteLength()no tiene nada que ver con xo y, hará DisplayWebsiteLength()que se ejecute "en segundo plano", como

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Obviamente ese es un ejemplo estúpido, pero ¿estoy en lo correcto o estoy totalmente confundido o qué?

(Además, estoy confundido acerca de por qué sendery enunca se usan en el cuerpo de la función anterior).

usuario5648283
fuente
13
Esta es una buena explicación: blog.stephencleary.com/2013/11/there-is-no-thread.html
Jakub Lortz
sendery eestamos sugiriendo que este es en realidad un controlador de eventos, prácticamente el único lugar donde async voides deseable. Lo más probable es que esto se llame al hacer clic en un botón o algo así; el resultado es que esta acción ocurre de forma completamente asincrónica con respecto al resto de la aplicación. Pero todavía está todo en un hilo: el hilo de la interfaz de usuario (con un pequeño lapso de tiempo en un hilo de IOCP que publica la devolución de llamada en el hilo de la interfaz de usuario).
Luaan
Posible duplicado de la diferencia entre el programa Multithreading y Async en c #
Alireza Zojaji
3
Una nota muy importante sobre el DisplayWebsiteLengthejemplo de código: no debe usarse HttpClienten una usingdeclaración: bajo una carga pesada, el código puede agotar la cantidad de sockets disponibles, lo que resulta en errores de SocketException. Más información sobre instanciación incorrecta .
Gan
1
@JakubLortz No sé para quién es realmente el artículo. No es para principiantes, ya que requiere un buen conocimiento sobre hilos, interrupciones, cosas relacionadas con la CPU, etc. No es para usuarios avanzados, ya que para ellos ya está claro. Estoy seguro de que no ayudará a nadie a comprender de qué se trata: un nivel de abstracción demasiado alto.
Loreno

Respuestas:

589

Tu malentendido es extremadamente común. A muchas personas se les enseña que los subprocesos múltiples y la asincronía son lo mismo, pero no lo son.

Una analogía generalmente ayuda. Estás cocinando en un restaurante. Entra una orden de huevos y tostadas.

  • Sincrónico: cocinas los huevos, luego cocinas las tostadas.
  • Asíncrono, de un solo hilo: comienza a cocinar los huevos y establece un temporizador. Empiezas a cocinar las tostadas y configuras un temporizador. Mientras ambos están cocinando, tú limpias la cocina. Cuando se apagan los temporizadores, retira los huevos del fuego y las tostadas de la tostadora y sírvelas.
  • Asíncrono, multiproceso: contrata a dos cocineros más, uno para cocinar huevos y otro para cocinar tostadas. Ahora tiene el problema de coordinar a los cocineros para que no entren en conflicto en la cocina al compartir recursos. Y tienes que pagarles.

¿Tiene sentido ahora que el subprocesamiento múltiple es solo un tipo de asincronía? Enhebrar se trata de trabajadores; La asincronía se trata de tareas . En los flujos de trabajo multiproceso, asigna tareas a los trabajadores. En los flujos de trabajo asincrónicos de subproceso único, tiene un gráfico de tareas donde algunas tareas dependen de los resultados de otras; a medida que cada tarea se completa, invoca el código que programa la próxima tarea que puede ejecutarse, dados los resultados de la tarea que se acaba de completar. Pero (con suerte) solo necesita un trabajador para realizar todas las tareas, no un trabajador por tarea.

Ayudará a darse cuenta de que muchas tareas no están vinculadas al procesador. Para las tareas vinculadas al procesador, tiene sentido contratar tantos trabajadores (subprocesos) como procesadores, asignar una tarea a cada trabajador, asignar un procesador a cada trabajador y hacer que cada procesador haga el trabajo de nada más que calcular el resultado como lo mas rapido posible. Pero para las tareas que no están esperando en un procesador, no necesita asignar un trabajador en absoluto. Usted sólo tiene que esperar para que el mensaje llegue de que el resultado está disponible y hacer otra cosa mientras espera . Cuando llegue ese mensaje, puede programar la continuación de la tarea completada como lo siguiente en su lista de tareas pendientes para marcar.

Así que veamos el ejemplo de Jon con más detalle. ¿Lo que pasa?

  • Alguien invoca DisplayWebSiteLength. ¿OMS? No nos importa
  • Establece una etiqueta, crea un cliente y le pide al cliente que busque algo. El cliente devuelve un objeto que representa la tarea de buscar algo. Esa tarea está en progreso.
  • ¿Está en progreso en otro hilo? Probablemente no. Lea el artículo de Stephen sobre por qué no hay hilo.
  • Ahora esperamos la tarea. ¿Lo que pasa? Verificamos si la tarea se ha completado entre el momento en que la creamos y la esperamos. En caso afirmativo, buscamos el resultado y seguimos corriendo. Supongamos que no se ha completado. Suscribimos el resto de este método como la continuación de esa tarea y regresamos .
  • Ahora el control ha vuelto a la persona que llama. ¿Qué hace? Lo que quiera
  • Ahora suponga que la tarea se completa. ¿Cómo hizo eso? Tal vez se estaba ejecutando en otro subproceso, o tal vez la persona que llamó a la que acabamos de regresar permitió que se ejecutara hasta el final en el subproceso actual. De todos modos, ahora tenemos una tarea completada.
  • La tarea completada solicita el hilo correcto, nuevamente, probablemente el único hilo, para ejecutar la continuación de la tarea.
  • El control pasa inmediatamente al método que acabamos de dejar en el punto de espera. Ahora no es el resultado disponible, así que podemos asignar texty ejecutar el resto del método.

Es como en mi analogía. Alguien te pide un documento. Envías por correo el documento y sigues haciendo otro trabajo. Cuando llegue por correo, se le indicará, y cuando lo desee, hará el resto del flujo de trabajo: abra el sobre, pague los gastos de envío, lo que sea. No necesita contratar a otro trabajador para que haga todo eso por usted.

Eric Lippert
fuente
8
@ user5648283: El hardware es el nivel incorrecto para pensar en las tareas. Una tarea es simplemente un objeto que (1) representa que un valor estará disponible en el futuro y (2) puede ejecutar código (en el hilo correcto) cuando ese valor esté disponible . Cómo cualquier tarea individual obtiene el resultado en el futuro depende de ello. Algunos usarán hardware especial como "discos" y "tarjetas de red" para hacer eso; algunos usarán hardware como CPU.
Eric Lippert
13
@ user5648283: Nuevamente, piensa en mi analogía. Cuando alguien le pide que cocine huevos y tostadas, utiliza un hardware especial (una estufa y una tostadora) y puede limpiar la cocina mientras el hardware está haciendo su trabajo. Si alguien le pide huevos, tostadas y una crítica original de la última película de Hobbit, puede escribir su reseña mientras se cocinan los huevos y las tostadas, pero no necesita usar hardware para eso.
Eric Lippert
99
@ user5648283: Ahora, en cuanto a su pregunta sobre "reorganizar el código", considere esto. Supongamos que tiene un método P que tiene un rendimiento de rendimiento y un método Q que hace un foreach sobre el resultado de P. Pase por el código. Verás que corremos un poco de Q, luego un poco de P y luego un poco de Q ... ¿Entiendes el punto de eso? esperar es esencialmente rendimiento de rendimiento en disfraces . ¿Ahora está más claro?
Eric Lippert el
10
La tostadora es hardware. El hardware no necesita un hilo para repararlo; los discos y las tarjetas de red y demás se ejecutan a un nivel muy inferior al de los subprocesos del sistema operativo.
Eric Lippert el
55
@ ShivprasadKoirala: Eso no es del todo cierto . Si crees eso, entonces tienes algunas creencias muy falsas sobre la asincronía . El punto de asincronía en C # es que no crea un hilo.
Eric Lippert
27

Javascript en el navegador es un gran ejemplo de un programa asincrónico que no tiene hilos.

No tiene que preocuparse de que varias partes del código toquen los mismos objetos al mismo tiempo: cada función terminará de ejecutarse antes de que cualquier otro javascript pueda ejecutarse en la página.

Sin embargo, al hacer algo como una solicitud AJAX, no se está ejecutando ningún código, por lo que otros javascript pueden responder a cosas como eventos de clic hasta que esa solicitud regrese e invoque la devolución de llamada asociada con ella. Si uno de estos otros controladores de eventos todavía se está ejecutando cuando vuelve la solicitud AJAX, no se llamará a su controlador hasta que finalicen. Solo se está ejecutando un "hilo" de JavaScript, aunque es posible que pause efectivamente lo que estaba haciendo hasta que tenga la información que necesita.

En las aplicaciones de C #, ocurre lo mismo cada vez que se trata de elementos de la interfaz de usuario: solo se le permite interactuar con los elementos de la interfaz de usuario cuando está en el hilo de la interfaz de usuario. Si el usuario hizo clic en un botón y usted quería responder leyendo un archivo grande del disco, un programador sin experiencia podría cometer el error de leer el archivo dentro del controlador de eventos de clic, lo que haría que la aplicación se "congelara" hasta que el archivo terminó de cargarse porque no está permitido responder a más clics, desplazamientos o cualquier otro evento relacionado con la interfaz de usuario hasta que se libere ese hilo.

Una opción que los programadores podrían usar para evitar este problema es crear un nuevo hilo para cargar el archivo, y luego decirle al código de ese hilo que cuando se carga el archivo necesita ejecutar el código restante en el hilo de la interfaz de usuario nuevamente para que pueda actualizar los elementos de la interfaz de usuario basado en lo que encontró en el archivo. Hasta hace poco, este enfoque era muy popular porque era lo que las bibliotecas y el lenguaje de C # facilitaban, pero es fundamentalmente más complicado de lo que tiene que ser.

Si piensa en lo que hace la CPU cuando lee un archivo a nivel del hardware y el sistema operativo, básicamente emite una instrucción para leer datos del disco en la memoria y golpear el sistema operativo con una "interrupción "cuando se completa la lectura. En otras palabras, leer desde el disco (o cualquier E / S realmente) es una operación inherentemente asíncrona . El concepto de un subproceso que espera que se complete esa E / S es una abstracción que los desarrolladores de la biblioteca crearon para facilitar la programación. No es necesario.

Ahora, la mayoría de las operaciones de E / S en .NET tienen un ...Async()método correspondiente que puede invocar, que devuelve Taskcasi de inmediato. Puede agregar devoluciones de llamada a esto Taskpara especificar el código que desea ejecutar cuando se complete la operación asincrónica. También puede especificar en qué subproceso desea que se ejecute ese código, y puede proporcionar un token que la operación asincrónica puede verificar de vez en cuando para ver si decidió cancelar la tarea asincrónica, dándole la oportunidad de detener su trabajo rápidamente Y con gracia.

Hasta que async/awaitse agregaron las palabras clave, C # era mucho más obvio sobre cómo se invoca el código de devolución de llamada, porque esas devoluciones de llamada eran en forma de delegados que usted asoció con la tarea. Para poder seguir brindándole el beneficio de utilizar la ...Async()operación, al tiempo que evita la complejidad del código, async/awaitabstrae la creación de esos delegados. Pero todavía están allí en el código compilado.

Por lo tanto, puede hacer que su controlador de eventos de IU awaitfuncione de E / S, liberando el subproceso de la IU para hacer otras cosas y volviendo más o menos automáticamente al subproceso de la IU una vez que haya terminado de leer el archivo, sin tener que hacerlo crear un nuevo hilo

StriplingWarrior
fuente
Solo se está ejecutando un "hilo" de JavaScript , ya no es cierto con Web Workers .
oleksii
66
@oleksii: Eso es técnicamente cierto, pero no iba a entrar en eso porque la API de Web Workers en sí es asíncrona, y Web Workers no puede afectar directamente los valores de JavaScript o el DOM en la página web en la que se invocan from, lo que significa que el segundo párrafo crucial de esta respuesta aún es cierto. Desde la perspectiva del programador, hay poca diferencia entre invocar a un trabajador web e invocar una solicitud AJAX.
StriplingWarrior