¿Llamadas asincrónicas y sin bloqueo? también entre bloqueo y sincrónico
¿Cuál es la diferencia entre llamadas asincrónicas y sin bloqueo? ¿También entre bloqueo y llamadas sincrónicas (con ejemplos, por favor)?
En muchas circunstancias son nombres diferentes para una misma cosa, pero en algunos contextos son bastante diferentes. Entonces depende. La terminología no se aplica de forma totalmente coherente en toda la industria del software.
Por ejemplo, en la API de sockets clásica, un socket sin bloqueo es aquel que simplemente regresa inmediatamente con un mensaje de error especial "bloquearía", mientras que un socket con bloqueo se habría bloqueado. Debe utilizar una función independiente, como select
o, poll
para saber cuándo es un buen momento para volver a intentarlo.
Pero los sockets asíncronos (compatibles con los sockets de Windows), o el patrón IO asíncrono usado en .NET, son más convenientes. Llamas a un método para iniciar una operación y el marco te devuelve la llamada cuando termina. Incluso aquí existen diferencias básicas. Los sockets Win32 asincrónicos "ordenan" sus resultados en un subproceso de GUI específico pasando mensajes de Windows, mientras que .NET IO asincrónico tiene subprocesos libres (no sabe en qué subproceso se llamará su devolución de llamada).
Por eso no siempre significan lo mismo . Para resumir el ejemplo del socket, podríamos decir:
- Bloquear y sincrónico significan lo mismo: llamas a la API, esta cuelga el hilo hasta que tiene algún tipo de respuesta y te la devuelve.
- Sin bloqueo significa que si no se puede devolver una respuesta rápidamente, la API regresa inmediatamente con un error y no hace nada más. Por lo tanto, debe haber alguna forma relacionada de consultar si la API está lista para ser llamada (es decir, simular una espera de manera eficiente, para evitar el sondeo manual en un bucle cerrado).
- Asincrónico significa que la API siempre regresa inmediatamente, después de haber iniciado un esfuerzo "en segundo plano" para cumplir con su solicitud, por lo que debe haber alguna forma relacionada de obtener el resultado.
sincrónico/asincrónico es describir la relación entre dos módulos.
bloqueo / no bloqueo es describir la situación de un módulo.
Un ejemplo:
Módulo X: "Yo".
Módulo Y: "librería".
X pregunta a Y: ¿tienes un libro llamado "cartilla de c++"?
bloqueo: antes de que Y responda X, X sigue esperando la respuesta. Ahora X (un módulo) está bloqueando. ¿X e Y son dos subprocesos o dos procesos o un subproceso o un proceso? NO lo sabemos.
sin bloqueo: antes de que Y responda X, X simplemente se va de allí y hace otras cosas. ¿X puede regresar cada dos minutos para comprobar si Y ha terminado su trabajo? ¿O X no volverá hasta que Y lo llame? No lo sabemos. Sólo sabemos que X puede hacer otras cosas antes de que Y termine su trabajo. Aquí X (un módulo) no es bloqueante. ¿X e Y son dos subprocesos, dos procesos o un proceso? NO lo sabemos. PERO estamos seguros de que X e Y no pueden ser un solo hilo.
sincrónico: antes de que Y responda X, X sigue esperando la respuesta. Significa que X no puede continuar hasta que Y termine su trabajo. Ahora decimos: X e Y (dos módulos) son sincrónicos. ¿X e Y son dos subprocesos o dos procesos o un subproceso o un proceso? NO lo sabemos.
asincrónico: antes de que Y responda X, X se va de allí y X puede hacer otros trabajos. X no volverá hasta que Y lo llame. Ahora decimos: X e Y (dos módulos) son asíncronos. ¿X e Y son dos subprocesos, dos procesos o un proceso? NO lo sabemos. PERO estamos seguros de que X e Y no pueden ser un solo hilo.
Preste atención a las dos oraciones en negrita anteriores. ¿Por qué la oración en negrita en 2) contiene dos casos mientras que la oración en negrita en 4) contiene solo un caso? Esta es la clave de la diferencia entre no bloqueante y asincrónico.
Permítanme intentar explicar las cuatro palabras de otra manera:
bloqueo: ¡Dios mío, estoy congelado! ¡No puedo moverme! Tengo que esperar a que suceda ese evento específico. Si eso sucede, ¡sería salvo!
sin bloqueo: me dijeron que tenía que esperar a que ocurriera ese evento específico. OK, lo entiendo y prometo que esperaría por eso. Pero mientras espero, todavía puedo hacer otras cosas, no estoy congelado, sigo vivo, puedo saltar, puedo caminar, puedo cantar una canción, etc.
sincrónico: Mi mamá va a cocinar, me manda a comprar carne. Le acabo de decir a mi mamá: ¡Somos sincrónicos! Lo siento mucho, pero tienes que esperar aunque necesite 100 años para recuperar algo de carne...
asincrónico: Haremos una pizza, necesitamos tomate y queso. Ahora digo: vamos de compras. Yo compraré unos tomates y tú comprarás queso. No necesitamos esperarnos unos a otros porque somos asincrónicos.
A continuación se muestra un ejemplo típico sobre sin bloqueo y sincrónico:
// thread X
while (true)
{
msg = recv(Y, NON_BLOCKING_FLAG);
if (msg is not empty)
{
break;
}
else
{
sleep(2000); // 2 sec
}
}
// thread Y
// prepare the book for X
send(X, book);
Puedes ver que este diseño no es bloqueante (puedes decir que la mayoría de las veces este bucle hace algo sin sentido, pero a los ojos de la CPU, X se está ejecutando, lo que significa que X no es bloqueante. Si quieres, puedes reemplazarlo sleep(2000)
con cualquier otro código) mientras que X e Y ( dos módulos ) son sincrónicos porque X no puede continuar haciendo ninguna otra cosa (X no puede salir del bucle) hasta que obtenga el libro de Y.
Normalmente, en este caso, hacer que X bloquee es mucho mejor porque el no bloqueo gasta muchos recursos en un bucle estúpido. Pero este ejemplo es bueno para ayudarle a comprender el hecho: no bloqueo no significa asincrónico.
Las cuatro palabras sí nos confunden fácilmente, lo que debemos recordar es que las cuatro palabras sirven para el diseño de arquitectura. Aprender a diseñar una buena arquitectura es la única forma de distinguirlos.
Por ejemplo, podemos diseñar este tipo de arquitectura:
// Module X = Module X1 + Module X2
// Module X1
while (true)
{
msg = recv(many_other_modules, NON_BLOCKING_FLAG);
if (msg is not null)
{
if (msg == "done")
{
break;
}
// create a thread to process msg
}
else
{
sleep(2000); // 2 sec
}
}
// Module X2
broadcast("I got the book from Y");
// Module Y
// prepare the book for X
send(X, book);
En el ejemplo aquí, podemos decir que
- X1 no es bloqueante
- X1 y X2 son sincrónicos
- X e Y son asíncronos
Si lo necesitas, también puedes describir esos hilos creados en X1 con las cuatro palabras.
Una vez más: las cuatro palabras sirven para el diseño de arquitectura . Entonces, lo que necesitamos es hacer una arquitectura adecuada, en lugar de distinguir las cuatro palabras como un abogado de idiomas. Si hay algunos casos en los que no puedes distinguir las cuatro palabras con mucha claridad, debes olvidarte de las cuatro palabras y usar tus propias palabras para describir tu arquitectura.
Entonces, las cosas más importantes son: ¿cuándo usamos síncrono en lugar de asíncrono? ¿Cuándo utilizamos el bloqueo en lugar del no bloqueo? ¿Hacer el bloqueo X1 es mejor que no bloquear? ¿Hacer que X e Y sean sincrónicos es mejor que asincrónicos? ¿Por qué Nginx no bloquea? ¿Por qué bloquea Apache? Estas preguntas son las que debes resolver.
Para hacer una buena elección, debes analizar tu necesidad y probar el rendimiento de diferentes arquitecturas. No existe una arquitectura que sea adecuada para diversas necesidades.
- Asincrónico se refiere a algo que se hace en paralelo , digamos que es otro hilo.
- El no bloqueo a menudo se refiere a sondeo , es decir, verificar si una condición dada se cumple (el socket es legible, el dispositivo tiene más datos, etc.)
Sincrónico se define como algo que sucede al mismo tiempo (en un momento predecible o en un orden predecible).
Asincrónico se define como no suceder al mismo tiempo. (con tiempos impredecibles o con pedidos impredecibles).
Esto es lo que causa la primera confusión, que es que asíncrono es una especie de esquema de sincronización, y sí, se usa para significar eso, pero en realidad describe procesos que suceden de manera impredecible con respecto a cuándo o en qué orden se ejecutan. Y tales eventos a menudo necesitan sincronizarse para que se comporten correctamente, donde existen múltiples esquemas de sincronización para hacerlo, uno de ellos llamado bloqueo , otro llamado no bloqueo y otro llamado confusamente asíncrono .
Como puede ver, todo el problema consiste en encontrar una manera de sincronizar un comportamiento asincrónico, porque tiene alguna operación que necesita la respuesta de otra antes de que pueda comenzar. Entonces es un problema de coordinación, ¿cómo sabrás que ahora puedes iniciar esa operación?
La solución más sencilla se conoce como bloqueo.
El bloqueo es cuando simplemente elige esperar a que se haga lo otro y le devuelva una respuesta antes de pasar a la operación que lo necesitaba.
Entonces, si necesitas poner mantequilla sobre una tostada, primero debes tostar la carne. La forma en que los coordinarías es que primero tostarías la carne, luego mirarías fijamente la tostadora hasta que reviente la tostada, y luego procederías a ponerles mantequilla.
Es la solución más sencilla y funciona muy bien. No hay ninguna razón real para no usarlo, a menos que también tengas otras cosas que hacer que no requieran coordinación con las operaciones. Por ejemplo, lavar algunos platos. ¿Por qué esperar sin hacer nada mirando la tostadora constantemente hasta que se reviente la tostada, cuando sabes que llevará un poco de tiempo y que podrías lavar un plato entero mientras termina?
Ahí es donde entran en juego otras dos soluciones conocidas respectivamente como sin bloqueo y asincrónicas.
El no bloqueo es cuando eliges hacer otras cosas no relacionadas mientras esperas a que se realice la operación. Volviendo a comprobar la disponibilidad de la respuesta como mejor le parezca.
Entonces, en lugar de mirar la tostadora para que explote. Ve y lava un plato entero. Y luego echas un vistazo a la tostadora para ver si las tostadas han reventado. Si no lo han hecho, lava otro plato y revisa la tostadora entre cada plato. Cuando ves que las tostadas han reventado, dejas de lavar los platos, y en su lugar coges las tostadas y pasas a ponerles mantequilla.
Sin embargo, tener que controlar constantemente las tostadas puede resultar molesto, imagina que la tostadora está en otra habitación. Entre platos pierdes el tiempo yendo a esa otra habitación a revisar las tostadas.
Aquí viene asincrónico.
Asincrónico es cuando eliges hacer otras cosas no relacionadas mientras esperas a que se realice la operación. Sin embargo, en lugar de verificarlo, delega el trabajo de verificar a otra cosa, podría ser la operación en sí o un observador, y hace que esa cosa le notifique y posiblemente lo interrumpa cuando la respuesta esté disponible para que pueda continuar con la otra operación que lo necesitaba.
Es una terminología extraña. No tiene mucho sentido, ya que todas estas soluciones son formas de crear una coordinación sincrónica de tareas dependientes. Por eso prefiero llamarlo acontecimiento.
Entonces, para este, decides actualizar tu tostadora para que emita un pitido cuando las tostadas estén listas. Estás escuchando constantemente, incluso mientras estás lavando los platos. Al escuchar el pitido, haces cola en tu memoria de que tan pronto como termines de lavar tu plato actual, te detendrás y irás a poner mantequilla en la tostada. O puedes optar por interrumpir el lavado del plato actual y ocuparte de las tostadas de inmediato.
Si tiene problemas para escuchar el pitido, puede pedirle a su compañero que vigile la tostadora por usted y le avise cuando la tostada esté lista. Su compañero puede elegir cualquiera de las tres estrategias anteriores para coordinar su tarea de vigilar la tostadora y avisarle cuando esté lista.
En una nota final, es bueno entender que si bien el no bloqueo y la sincronización (o lo que prefiero llamar evento) te permiten hacer otras cosas mientras esperas, no es necesario hacerlo. Puede optar por verificar constantemente el estado de una llamada sin bloqueo y no hacer nada más. Sin embargo, esto suele ser peor que bloquear (como mirar la tostadora, luego alejarla y luego volver a mirarla hasta que esté lista), por lo que muchas API sin bloqueo te permiten pasar a un modo de bloqueo desde ella. Para eventos, puede esperar inactivo hasta que se le notifique. La desventaja en ese caso es que agregar la notificación fue complejo y potencialmente costoso para empezar. Tenías que comprar una tostadora nueva con función de pitido o convencer a tu pareja para que la cuidara por ti.
Y una cosa más: es necesario darse cuenta de las ventajas y desventajas que ofrecen los tres. Obviamente, uno no es mejor que los demás. Piensa en mi ejemplo. Si tu tostadora es tan rápida no tendrás tiempo de lavar un plato, ni siquiera de empezar a lavarlo, así de rápida es tu tostadora. En ese caso, empezar a hacer otra cosa es sólo una pérdida de tiempo y esfuerzo. El bloqueo servirá. Del mismo modo, si lavar un plato tardará 10 veces más que tostarlo. Tienes que preguntarte ¿qué es más importante hacer? La tostada puede estar fría y dura en ese momento, no vale la pena, bloquearla también servirá. O deberías elegir cosas más rápidas para hacer mientras esperas. Obviamente hay más, pero mi respuesta ya es bastante larga. Mi punto es que debes pensar en todo eso y en las complejidades de implementar cada uno para decidir si vale la pena y si realmente mejorará tu rendimiento.
Editar:
Aunque esto ya es largo, también quiero que esté completo, así que agregaré dos puntos más.
- También suele existir un cuarto modelo conocido como multiplexado . Esto es cuando mientras esperas una tarea, comienzas otra, y mientras esperas ambas, comienzas una más, y así sucesivamente, hasta que tienes muchas tareas iniciadas y luego esperas inactivo, pero en todas. a ellos. Entonces, tan pronto como haya terminado, puede continuar con el manejo de su respuesta y luego volver a esperar a los demás. Se conoce como multiplexado, porque mientras esperas, debes comprobar cada tarea una tras otra para ver si están terminadas, ad vitam, hasta que una lo esté. Es una especie de extensión además del no bloqueo normal.
En nuestro ejemplo sería como encender la tostadora, luego el lavavajillas, luego el microondas, etc. Y luego esperar a cualquiera de ellos. Donde revisarías la tostadora para ver si está lista, si no, revisarías el lavavajillas, si no, el microondas, y alrededor nuevamente.
- Aunque creo que es un gran error, a menudo se utiliza sincrónico para referirse a una cosa a la vez. Y asíncrono muchas cosas a la vez. Por lo tanto, verá que el bloqueo y el no bloqueo sincrónicos se usan para referirse al bloqueo y al no bloqueo. Y el bloqueo y no bloqueo asincrónicos solían referirse a multiplexados y eventos.
Realmente no entiendo cómo llegamos allí. Pero cuando se trata de IO y Computación, síncrono y asíncrono a menudo se refieren a lo que se conoce mejor como no superpuesto y superpuesto. Es decir, asíncrono significa que IO y Computación se superponen, es decir, suceden al mismo tiempo. Mientras que sincrónicos significa que no lo son, por lo que suceden de forma secuencial. Para el no bloqueo síncrono, eso significaría que no inicia otra IO o Computación, simplemente espera y simula una llamada de bloqueo. Desearía que la gente dejara de abusar de lo sincrónico y asincrónico de esa manera. Así que no lo estoy fomentando.
Editar2:
Creo que mucha gente se confundió un poco con mi definición de sincrónico y asincrónico. Déjame intentar ser un poco más claro.
Sincrónico se define como algo que ocurre con tiempos y/u orden predecibles. Eso significa que sabes cuándo comenzará y terminará algo.
Asincrónico se define como que no sucede con tiempos y/o pedidos predecibles. Eso significa que no sabes cuándo comenzará y terminará algo.
Ambos pueden suceder en paralelo o simultáneamente, o pueden suceder de forma secuencial. Pero en el caso sincrónico, sabes exactamente cuándo sucederán las cosas, mientras que en el caso asincrónico no estás seguro exactamente de cuándo sucederán las cosas, pero aun así puedes establecer cierta coordinación que al menos garantice que algunas cosas sucederán solo después de otras. sucedido (sincronizando algunas partes del mismo).
Por lo tanto, cuando tiene procesos asincrónicos, la programación asincrónica le permite colocar algunas garantías de orden para que algunas cosas sucedan en la secuencia correcta, aunque no sepa cuándo comenzarán y terminarán.
Aquí hay un ejemplo: si necesitamos hacer A, entonces B y C pueden suceder en cualquier momento. En un modelo secuencial pero asincrónico puedes tener:
A -> B -> C
or
A -> C -> B
or
C -> A -> B
Cada vez que ejecutas el programa, puedes obtener uno diferente, aparentemente al azar. Ahora bien, esto sigue siendo secuencial, nada es paralelo o concurrente, pero no sabes cuándo comenzarán y terminarán las cosas, excepto que hayas hecho que B siempre suceda después de A.
Si agrega solo concurrencia (sin paralelismo), también puede obtener cosas como:
A<start> -> C<start> -> A<end> -> C<end> -> B<start> -> B<end>
or
C<start> -> A<start> -> C<end> -> A<end> -> B<start> -> B<end>
or
A<start> -> A<end> -> B<start> -> C<start> -> B<end> -> C<end>
etc...
Una vez más, no sabes realmente cuándo comenzarán y terminarán las cosas, pero lo has hecho para que B esté coordinado para comenzar siempre después de que A termine, pero eso no es necesariamente inmediatamente después de que A termine, es en algún momento desconocido después de que A termine. y B podría ocurrir en el medio total o parcialmente.
Y si agregas paralelismo, ahora tienes cosas como:
A<start> -> A<end> -> B<start> -> B<end> ->
C<start> -> C<keeps going> -> C<keeps going> -> C<end>
or
A<start> -> A<end> -> B<start> -> B<end>
C<start> -> C<keeps going> -> C<end>
etc...
Ahora, si miramos el caso sincrónico, en una configuración secuencial tendrías:
A -> B -> C
Y este es el orden siempre, cada vez que ejecutas el programa, obtienes A, luego B y luego C, aunque C conceptualmente a partir de los requisitos puede ocurrir en cualquier momento, en un modelo sincrónico aún defines exactamente cuándo comenzará y terminará. . Por supuesto, podrías especificarlo como:
C -> A -> B
en cambio, pero dado que es sincrónico, este orden será el orden cada vez que se ejecute el programa, a menos que haya cambiado el código nuevamente para cambiar el orden explícitamente.
Ahora, si agrega concurrencia a un modelo sincrónico, puede obtener:
C<start> -> A<start> -> C<end> -> A<end> -> B<start> -> B<end>
Y una vez más, este sería el orden sin importar cuántas veces ejecutes el programa. Y de manera similar, podría cambiarlo explícitamente en su código, pero sería coherente en toda la ejecución del programa.
Finalmente, si también agrega paralelismo a un modelo síncrono, obtiene:
A<start> -> A<end> -> B<start> -> B<end>
C<start> -> C<end>
Una vez más, este sería el caso en cada ejecución del programa. Un aspecto importante aquí es que para que sea completamente sincrónico de esta manera, significa que B debe comenzar después de que A y C terminen. Si C es una operación que puede completarse más rápido o más lento, digamos dependiendo de la potencia de la CPU de la máquina u otra consideración de rendimiento, para que sea sincrónica aún necesita hacerlo de modo que B espere a que finalice; de lo contrario, obtendrá un comportamiento asincrónico. Nuevamente, donde no todos los tiempos son deterministas.
Obtendrá mucho este tipo de sincronización al coordinar las operaciones de la CPU con el reloj de la CPU, y debe asegurarse de poder completar cada operación a tiempo para el siguiente ciclo de reloj; de lo contrario, deberá retrasar todo un reloj más. para darle espacio a que este termine, si no lo haces arruinas tu comportamiento sincrónico, y si las cosas dependieran de ese orden se romperían.
Finalmente, muchos sistemas tienen un comportamiento sincrónico y asincrónico combinado, por lo que si tiene algún tipo de evento inherentemente impredecible, como cuando un usuario hará clic en un botón o cuando una API remota devolverá una respuesta, pero necesita que las cosas estén garantizadas. Al realizar pedidos, básicamente necesitará una forma de sincronizar el comportamiento asincrónico para garantizar el orden y la sincronización según sea necesario. Algunas estrategias para sincronizarlas son de las que hablé anteriormente: bloqueo, no bloqueo, async , multiplexado, etc. Vea el énfasis en "async", esto es lo que quiero decir con la palabra confuso. Alguien decidió llamar "async" a una estrategia para sincronizar procesos asincrónicos. Esto luego hizo que la gente pensara erróneamente que asincrónico significaba concurrente y sincrónico significaba secuencial, o que de alguna manera bloquear era lo opuesto a asincrónico, mientras que como acabo de explicar, sincrónico y asincrónico en realidad es un concepto diferente que se relaciona con el tiempo de las cosas como si estuvieran en sincronizados (en el tiempo entre sí, ya sea en algún reloj compartido o en un orden predecible) o desincronizados (no en algún reloj compartido o en un orden impredecible). Mientras que la programación asincrónica es una estrategia para sincronizar dos eventos que son en sí mismos asincrónicos (que suceden en un momento y/o orden impredecible), y para los cuales necesitamos agregar algunas garantías de cuándo podrían suceder o al menos en qué orden.
Entonces nos quedan dos cosas que usan la palabra "asíncrono" en ellas:
- Procesos asincrónicos: procesos que no sabemos en qué momento empezarán y terminarán y, por tanto, en qué orden acabarían ejecutándose.
- Programación asincrónica: un estilo de programación que le permite sincronizar dos procesos asincrónicos mediante devoluciones de llamada o observadores que interrumpen al ejecutor para informarle que se ha hecho algo, de modo que pueda agregar un orden predecible entre los procesos.