La sincronicidad es un mito.

Hola a todos!

Hoy encontrará un texto largo sin imágenes (ligeramente acortado en comparación con el original), donde se analiza en detalle la tesis presentada en el encabezado. El veterano de Microsoft, Terry Crowley, describe la esencia de la programación asincrónica y explica por qué este enfoque es mucho más realista y más apropiado que el sincrónico y secuencial.

Aquellos que deseen o piensen en escribir un libro que aborde tales temas, escriban de manera personal.

La sincronicidad es un mito. No pasa nada al instante. Todo lleva tiempo.
Algunas características de los sistemas informáticos y los entornos de programación se basan fundamentalmente en el hecho de que los cálculos se producen en el mundo físico tridimensional y están limitados por los límites de la velocidad de la luz y las leyes de la termodinámica.

Tal arraigo en el mundo físico significa que algunos aspectos no pierden su relevancia incluso con el advenimiento de nuevas tecnologías que brindan nuevas oportunidades y acceso a nuevas fronteras de productividad. Siguen siendo válidas porque no son solo "opciones elegidas durante el diseño", sino la realidad subyacente del Universo físico.

La diferencia entre sincronismo y asincronía en el lenguaje y la creación de sistemas es solo un aspecto del diseño que tiene bases físicas profundas. La mayoría de los programadores comienzan inmediatamente a trabajar con dichos programas y lenguajes, donde la ejecución síncrona está implícita. De hecho, esto es tan natural que nadie lo menciona ni habla directamente. El término "síncrono" en este contexto significa que el cálculo se lleva a cabo de inmediato, como una serie de pasos sucesivos, y nada más sucede antes de que se complete. Ejecuto “c = a + b” “x = f(y)” , y nada más sucederá hasta que se complete esta instrucción.

Por supuesto, nada sucede instantáneamente en el Universo físico. Todos los procesos están asociados con algunos retrasos: debe navegar por la jerarquía de memoria, ejecutar un ciclo de procesador, leer información de una unidad de disco o conectarse a otro nodo a través de la red, lo que también causa retrasos en la transferencia de datos. Todo esto es una consecuencia fundamental de la velocidad de la luz y la propagación de la señal en tres dimensiones.

Todos los procesos llegan un poco tarde; todo lleva tiempo. Al definir algunos procesos como sincrónicos, nosotros, en esencia, decimos que vamos a ignorar este retraso y describiremos nuestro cálculo como instantáneo. De hecho, en los sistemas informáticos, a menudo se establece una infraestructura seria, que le permite continuar utilizando activamente el hardware básico, incluso cuando intentan optimizar la interfaz de programación presentando los eventos que ocurren en él como síncronos.

La idea misma de que la sincronización se proporciona utilizando un mecanismo especial y está asociada con los costos puede parecer ilógica para el programador, que está más acostumbrado al hecho de que es una asincronía que requiere un control externo activo. De hecho, esto es lo que sucede realmente cuando se proporciona una interfaz asincrónica: la asincronía fundamental genuina se abre para el programador un poco más claramente que antes, y tiene que procesarla manualmente, en lugar de confiar en un programa que pueda hacerlo automáticamente. La provisión directa de asincronía está asociada con costos adicionales para el programador, pero al mismo tiempo le permite distribuir de manera más competente los costos y compensaciones inherentes a esta área temática, y no dejar esto a merced de un sistema que equilibraría dichos costos y compensaciones. La interfaz asincrónica a menudo corresponde con mayor precisión a los eventos que ocurren físicamente en el sistema base y, en consecuencia, abre posibilidades de optimización adicionales.

Por ejemplo, el procesador y el sistema de memoria cuentan con una infraestructura justa responsable de leer y escribir datos en la memoria, teniendo en cuenta su jerarquía. En el nivel 1 (L1), el enlace de caché puede tomar varios nanosegundos, mientras que el enlace de memoria en sí debe atravesar L2, L3 y la memoria principal, que puede tomar cientos de nanosegundos. Si solo espera hasta que se resuelva el enlace de memoria, el procesador estará inactivo durante un porcentaje significativo del tiempo.

Se utilizan mecanismos serios para optimizar tales fenómenos: canalización con una vista principal del flujo de comandos, múltiples operaciones simultáneas de recuperación de la memoria y el almacenamiento de datos actual, predicción de rama e intentos para optimizar aún más el programa, incluso cuando salta a otra ubicación de memoria, control preciso de las barreras de memoria para garantizar que todo este complejo mecanismo continuará proporcionando un modelo de memoria consistente para un entorno de programación de nivel superior. Todas estas cosas se hacen en un esfuerzo por optimizar el rendimiento y aprovechar al máximo el hardware para ocultar estos retrasos de 10-100 nanosegundos en la jerarquía de la memoria y proporcionar un sistema en el que se supone que se produzca una ejecución sincrónica, mientras exprimen un rendimiento decente del núcleo del procesador.

No siempre está claro cuán efectivas son tales optimizaciones para un código en particular, y a menudo se requieren herramientas muy específicas para analizar el rendimiento para responder a esta pregunta. Tal trabajo analítico se proporciona en el desarrollo de algunos códigos muy valiosos (por ejemplo, como en el motor de conversión para Excel, algunas opciones de compresión en el núcleo o rutas criptográficas en el código).

Las operaciones con un retraso más significativo, por ejemplo, leer datos de un disco giratorio, requieren el uso de otros mecanismos. En tales casos, cuando solicite leer desde el disco del sistema operativo, será necesario cambiar completamente a otro hilo o proceso, y la solicitud sincrónica permanecerá sin enviar. Los altos costos de conmutación y soporte de este mecanismo como tal son aceptables, ya que la latencia oculta en este caso puede alcanzar varios milisegundos en lugar de nanosegundos. Tenga en cuenta: estos costos no se reducen a simplemente cambiar entre subprocesos, sino que incluyen el costo de toda la memoria y los recursos, que de hecho permanecen inactivos hasta que se completa la operación. Todos estos costos tienen que ir para proporcionar una interfaz supuestamente sincrónica.

Hay una serie de razones fundamentales por las que puede ser necesario revelar la asincronía básica real en el sistema y para las cuales sería preferible usar una interfaz asincrónica con un determinado componente, nivel o aplicación, incluso teniendo en cuenta la necesidad de hacer frente directamente a la creciente complejidad.

Concurrencia Si el recurso proporcionado está diseñado para un verdadero paralelismo, la interfaz asincrónica permite al cliente emitir varias solicitudes y gestionarlas de forma más simultánea, para hacer un uso completo de los recursos básicos.

Transportación . La forma habitual de reducir el retraso real en alguna interfaz es asegurarse de que varias solicitudes están esperando ser enviadas en un momento dado (cuánto depende realmente de esto en términos de rendimiento depende de dónde obtengamos la fuente del retraso). En cualquier caso, si el sistema está adaptado para la canalización, la demora real puede reducirse en un factor igual al número de solicitudes que esperan ser enviadas. Por lo tanto, puede llevar 10 ms completar una solicitud específica, pero si escribe 10 solicitudes en la tubería, la respuesta puede llegar cada milisegundo. El rendimiento total es una función de la canalización disponible, y no solo un retraso de "transferencia" por solicitud. Una interfaz síncrona que emite una solicitud y espera una respuesta siempre dará un mayor retraso de extremo a extremo.

Embalaje (local o remoto) . La interfaz asíncrona proporciona de forma más natural la implementación de un sistema de empaquetado de consultas, localmente o en un recurso remoto (nota: en este caso, el "disco" en el otro extremo de la interfaz de E / S puede ser "remoto"). El hecho es que la aplicación ya debería poder recibir la respuesta, y al mismo tiempo habrá algún retraso, ya que la aplicación no interrumpirá el procesamiento actual. Dicho procesamiento adicional se puede combinar con solicitudes adicionales que naturalmente se agruparían.

El procesamiento por lotes local puede proporcionar una transferencia más eficiente de series de solicitudes, o incluso la compresión y eliminación de solicitudes duplicadas directamente en la máquina local. Para poder acceder simultáneamente a un conjunto completo de solicitudes en un recurso remoto, puede ser necesaria una optimización seria. Un ejemplo clásico: un controlador de disco reordena una serie de operaciones de lectura y escritura para aprovechar la posición de la cabeza del disco en una placa giratoria y minimizar el tiempo de alimentación de la cabeza. En cualquier interfaz de almacenamiento de datos que opere a nivel de bloque, puede mejorar seriamente el rendimiento agrupando una serie de consultas en las que todas las operaciones de lectura y escritura caen en el mismo bloque.

Naturalmente, el empaquetado local también se puede implementar en la interfaz síncrona, pero para esto tendrá que "ocultar la verdad" en gran medida o empaquetar paquetes de programas como una característica especial de la interfaz, lo que puede complicar a todo el cliente. Un ejemplo clásico de ocultar la verdad es la E / S almacenada. La aplicación llama a “write(byte)” , y la interfaz devuelve el success , pero, de hecho, el registro en sí (así como la información sobre si se aprobó con éxito) no tendrá lugar hasta que el búfer se llene o se vacíe explícitamente, y esto sucede cuando el archivo está cerrado . Muchas aplicaciones pueden ignorar tales detalles: solo se produce un desastre cuando la aplicación necesita garantizar algunas secuencias de operaciones interactivas, así como una idea real de lo que está sucediendo en los niveles inferiores.

Desbloquear / Desatar . Uno de los usos más comunes de la asincronía en el contexto de las interfaces gráficas de usuario es evitar que el hilo principal de la interfaz de usuario se bloquee para que el usuario pueda continuar interactuando con la aplicación. Los retrasos en las operaciones a largo plazo (como las comunicaciones de red) no se pueden ocultar detrás de una interfaz síncrona. En este caso, el hilo de la interfaz de usuario debe gestionar explícitamente tales operaciones asincrónicas y hacer frente a la complejidad adicional que se introduce en el programa.

La interfaz de usuario es solo un ejemplo en el que el componente debe continuar respondiendo a solicitudes adicionales y, por lo tanto, no puede confiar en algún mecanismo estándar que oculte demoras para simplificar el trabajo del programador.
Un componente de servidor web que recibe nuevas conexiones a sockets, como regla general, transferirá muy rápidamente dicha conexión a otro componente asíncrono que proporciona comunicación en el socket y volverá a procesar nuevas solicitudes.

En los modelos síncronos, los componentes y sus modelos de procesamiento suelen estar estrechamente relacionados.
Las interacciones asincrónicas son un mecanismo utilizado a menudo para aflojar la unión .

Reducción de costos y gestión. Como se mencionó anteriormente, cualquier mecanismo para ocultar la asincronía implica cierta asignación de recursos y sobrecarga. Para una aplicación en particular, tal sobrecarga puede no ser aceptable, y el diseñador de esa aplicación debe encontrar una manera de controlar la asincronía natural.

Un ejemplo interesante es la historia de los servidores web. Los primeros servidores web (creados en Unix) generalmente usaban un proceso separado para administrar las solicitudes entrantes. Entonces este proceso podría leer esta conexión y escribirle, sucedió, en esencia, sincrónicamente. Dicho diseño se desarrolló y los costos se redujeron cuando los hilos comenzaron a usarse en lugar de los procesos, pero el modelo general de ejecución síncrona se conservó. En las opciones de diseño moderno, se reconoce que la atención principal no debe prestarse al modelo de cálculo, sino, en primer lugar, a la entrada / salida relacionada con la lectura y escritura al intercambiar información con una base de datos, sistema de archivos o transmitir información a través de una red, mientras se formula una respuesta . Por lo general, se usan colas de trabajo para esto, en las que se permite un cierto límite en el número de subprocesos, y en este caso, es posible construir más claramente la administración de recursos.

El éxito de NodeJS en el desarrollo de back-end se explica no solo por el soporte de este motor por parte de numerosos desarrolladores de JavaScript que crecieron creando interfaces web para clientes. En NodeJS, como en las secuencias de comandos del navegador, se presta gran atención al diseño de forma asíncrona, lo que va bien con las opciones típicas de carga del servidor: la administración de los recursos del servidor depende principalmente de E / S y no del procesamiento.

Hay otro aspecto interesante: tales compensaciones son más explícitas y mejor ajustadas por el desarrollador de la aplicación, si se adhiere al enfoque asincrónico. En el ejemplo con retrasos en la jerarquía de memoria, el retraso real (medido en ciclos de procesador en términos de una solicitud en memoria) aumentó dramáticamente durante varias décadas. Los desarrolladores de procesadores luchan por agregar nuevos niveles de caché y mecanismos adicionales que impulsan cada vez más el modelo de memoria proporcionado por el procesador para que la apariencia del procesamiento sincrónico se mantenga aún más.

El cambio de contexto en los límites de la E / S sincrónica es otro ejemplo en el que las compensaciones reales han cambiado drásticamente con el tiempo. El aumento en los ciclos del procesador es más rápido que la lucha contra los retrasos, y esto significa que ahora la aplicación pierde muchas más capacidades computacionales, mientras está inactiva en una forma bloqueada, esperando la finalización del IO. El mismo problema relacionado con el costo relativo de los compromisos ha llevado a los diseñadores de sistemas operativos a apegarse a esquemas de administración de memoria que son mucho más similares a los modelos anteriores con el intercambio de procesos (donde toda la imagen del proceso se carga completamente en la memoria, después de lo cual el proceso comienza), en lugar de intercambiar páginas Es muy difícil ocultar los retrasos que pueden ocurrir en el borde de cada página. El rendimiento total dramáticamente mejorado logrado con grandes solicitudes de E / S secuenciales (en comparación con el uso de solicitudes aleatorias) también contribuye a tales cambios.

Otros temas

Cancelar

La cancelación es un tema complejo . Históricamente, los sistemas orientados sincrónicamente hicieron un mal trabajo con el proceso de cancelación, y algunos ni siquiera admitieron la cancelación. La cancelación esencialmente tenía que ser diseñada "fuera de banda", para tal operación se requería llamar a un hilo separado de ejecución. Como alternativa, los modelos asincrónicos son adecuados, donde el soporte para la cancelación se organiza de forma más natural, en particular, se utiliza un enfoque tan trivial: simplemente ignora qué respuesta finalmente regresa (y si regresa en absoluto). La cancelación se vuelve cada vez más importante cuando aumenta la variabilidad de los retrasos, y en la práctica también aumenta la tasa de error, lo que da una muy buena porción histórica que demuestra cómo se desarrollaron nuestros entornos de red.

Regulación / Gestión de recursos

El diseño síncrono, por definición, impone cierta limitación, evitando que la aplicación emita solicitudes adicionales hasta que se complete la solicitud actual. En un diseño asincrónico, el estrangulamiento no ocurre por nada, por lo que a veces es necesario implementarlo explícitamente. Esta publicación describe la situación con Word Web App como un ejemplo, donde la transición del diseño sincrónico a asincrónico causó serios problemas con la administración de recursos. Si la aplicación utiliza una interfaz síncrona, es posible que no reconozca que la limitación está implícitamente incrustada en el código. Al eliminar dicha limitación implícita, es posible (o necesario) organizar la gestión de recursos de manera más explícita.

Tuve que lidiar con esto al comienzo de mi carrera cuando transportamos un editor de texto de la API gráfica síncrona de Sun a X Windows. Al utilizar la API de Sun, la operación de representación era sincrónica, de modo que el cliente no recuperaba el control hasta que se completaba. En X Windows, se envió una solicitud gráfica de forma asíncrona a través de una conexión de red y luego el servidor de visualización la ejecutó (que podría estar en la misma máquina o en otra diferente).

Para garantizar un buen rendimiento interactivo, nuestra aplicación debe proporcionar algo de representación (es decir, asegurarse de que la línea donde está el cursor ahora se actualiza y representa), y luego verifica si hay alguna otra entrada de teclado que deba leerse. , ( , ), , . API. , , - . , . UI , .

, 30 (-, Facebook iPhone ). – ( , ), , . , , .



, . , Microsoft, , API – , , . , , – : «, !» , , .

, . – , . , : , , , . , - . , , async/await . «» , , , JavaScript. : , . Async/await , , . . , , , .

. , , . , , , . , , ( !). () , , .

, . , . async/await, , , , .

, , , – . , . – , , , ( , – Word Excel). , , - , , , .
, , , , .
, – . .

Conclusiones

. – , , . , , , . , ; , .

Source: https://habr.com/ru/post/es423573/


All Articles