En informática, la E/S asincrónica (también E/S no secuencial ) es una forma de procesamiento de entrada/salida que permite que otros procesos continúen antes de que finalice la operación de E/S. Un nombre utilizado para la E/S asincrónica en la API de Windows es E/S superpuesta .
Las operaciones de entrada y salida (E/S) en una computadora pueden ser extremadamente lentas en comparación con el procesamiento de datos. Un dispositivo de E/S puede incorporar dispositivos mecánicos que deben moverse físicamente, como un disco duro que busca una pista para leer o escribir; esto suele ser órdenes de magnitud más lento que la conmutación de la corriente eléctrica. Por ejemplo, durante una operación de disco que tarda diez milisegundos en realizarse, un procesador con una frecuencia de reloj de un gigahercio podría haber realizado diez millones de ciclos de procesamiento de instrucciones.
Un enfoque simple para la E/S sería iniciar el acceso y luego esperar a que se complete. Pero este enfoque, llamado E/S sincrónica o E/S de bloqueo , bloquearía el progreso de un programa mientras la comunicación está en curso, dejando inactivos los recursos del sistema . Cuando un programa realiza muchas operaciones de E/S (como un programa que depende principalmente o en gran medida de la entrada del usuario ), esto significa que el procesador puede pasar casi todo su tiempo inactivo esperando que se completen las operaciones de E/S.
Como alternativa, es posible iniciar la comunicación y luego realizar un procesamiento que no requiere que se complete la operación de E/S. Este enfoque se denomina entrada/salida asíncrona. Cualquier tarea que dependa de que se haya completado la operación de E/S (esto incluye tanto el uso de los valores de entrada como las operaciones críticas que pretenden garantizar que se ha completado una operación de escritura) aún necesita esperar a que se complete la operación de E/S y, por lo tanto, sigue bloqueada, pero otros procesamientos que no dependen de la operación de E/S pueden continuar.
Existen muchas funciones de sistemas operativos para implementar E/S asincrónicas en muchos niveles. De hecho, una de las principales funciones de todos los sistemas operativos, salvo los más rudimentarios , es realizar al menos alguna forma de E/S asincrónica básica, aunque esto puede no ser particularmente evidente para el usuario o el programador. En la solución de software más simple, el estado del dispositivo de hardware se sondea a intervalos para detectar si el dispositivo está listo para su próxima operación. (Por ejemplo, el sistema operativo CP/M se construyó de esta manera. Su semántica de llamadas al sistema no requería una estructura de E/S más elaborada que esta, aunque la mayoría de las implementaciones eran más complejas y, por lo tanto, más eficientes). El acceso directo a memoria (DMA) puede aumentar en gran medida la eficiencia de un sistema basado en sondeo, y las interrupciones de hardware pueden eliminar por completo la necesidad de sondeo. Los sistemas operativos multitarea pueden explotar la funcionalidad proporcionada por las interrupciones de hardware, al tiempo que ocultan al usuario la complejidad del manejo de las interrupciones. El spooling fue una de las primeras formas de multitarea diseñadas para explotar la E/S asincrónica. Por último, las API de E/S asincrónicas explícitas y de subprocesamiento múltiple dentro de los procesos de usuario pueden explotar aún más la E/S asincrónica, a costa de una complejidad de software adicional.
La E/S asincrónica se utiliza para mejorar la eficiencia energética y, en algunos casos, el rendimiento. Sin embargo, en algunos casos puede tener efectos negativos en la latencia y el rendimiento.
Formas de E/S y ejemplos de funciones POSIX:
Todas las formas de E/S asincrónicas exponen las aplicaciones a posibles conflictos de recursos y fallos asociados. Para evitarlo, se requiere una programación cuidadosa (que suele utilizar exclusión mutua , semáforos , etc.).
Al exponer la E/S asincrónica a las aplicaciones, existen algunas clases generales de implementación. La forma de la API proporcionada a la aplicación no necesariamente corresponde con el mecanismo que realmente proporciona el sistema operativo; las emulaciones son posibles. Además, una sola aplicación puede utilizar más de un método, según sus necesidades y los deseos de sus programadores. Muchos sistemas operativos proporcionan más de uno de estos mecanismos, es posible que algunos los proporcionen todos.
Disponible en los primeros sistemas operativos Unix. En un sistema operativo multitarea , el procesamiento se puede distribuir entre diferentes procesos, que se ejecutan de forma independiente, tienen su propia memoria y procesan sus propios flujos de E/S; estos flujos suelen estar conectados en tuberías . Los procesos son bastante costosos de crear y mantener, [ cita requerida ] por lo que esta solución solo funciona bien si el conjunto de procesos es pequeño y relativamente estable. También supone que los procesos individuales pueden operar de forma independiente, además de procesar la E/S de los demás; si necesitan comunicarse de otras formas, coordinarlos puede resultar difícil. [ cita requerida ]
Una extensión de este enfoque es la programación de flujo de datos , que permite redes más complicadas que solo las cadenas que admiten las tuberías.
Variaciones:
El sondeo proporciona una API sincrónica sin bloqueo que se puede utilizar para implementar alguna API asincrónica. Está disponible en los sistemas Unix y Windows tradicionales . Su principal problema es que puede desperdiciar tiempo de CPU sondeando repetidamente cuando no hay nada más que hacer para el proceso emisor, lo que reduce el tiempo disponible para otros procesos. Además, debido a que una aplicación de sondeo es esencialmente de un solo subproceso, es posible que no pueda aprovechar por completo el paralelismo de E/S del que es capaz el hardware.
Disponible en BSD Unix y casi cualquier otra cosa con una pila de protocolo TCP/IP que utilice o esté modelada a partir de la implementación de BSD. Una variación del tema del sondeo, un bucle de selección utiliza la select
llamada del sistema para dormir hasta que se produzca una condición en un descriptor de archivo (por ejemplo, cuando los datos están disponibles para leer), se produzca un tiempo de espera o se reciba una señalselect
(por ejemplo, cuando un proceso secundario muere). Al examinar los parámetros de retorno de la llamada, el bucle descubre qué descriptor de archivo ha cambiado y ejecuta el código apropiado. A menudo, para facilitar su uso, el bucle de selección se implementa como un bucle de eventos , tal vez utilizando funciones de devolución de llamada ; la situación se presta particularmente bien a la programación impulsada por eventos .
Aunque este método es confiable y relativamente eficiente, depende en gran medida del paradigma Unix de que " todo es un archivo "; cualquier E/S de bloqueo que no involucre un descriptor de archivo bloqueará el proceso. El bucle de selección también depende de poder involucrar a todas las E/S en la select
llamada central; las bibliotecas que realizan su propia E/S son particularmente problemáticas en este sentido. Un problema potencial adicional es que las operaciones de selección y de E/S aún están lo suficientemente desacopladas como para que el resultado de la selección pueda ser efectivamente una mentira: si dos procesos están leyendo desde un único descriptor de archivo (posiblemente un mal diseño), la selección puede indicar la disponibilidad de datos leídos que han desaparecido en el momento en que se emite la lectura, lo que resulta en un bloqueo; si dos procesos están escribiendo en un único descriptor de archivo (no es tan poco común), la selección puede indicar capacidad de escritura inmediata pero la escritura puede seguir bloqueando, porque el otro proceso ha llenado un búfer en el ínterin, o debido a que la escritura es demasiado grande para el búfer disponible o de otras maneras no adecuadas para el destinatario.
El bucle de selección no alcanza la máxima eficiencia del sistema posible con, por ejemplo, el método de colas de finalización , porque la semántica de la select
llamada, que permite un ajuste por llamada del conjunto de eventos aceptable, consume cierta cantidad de tiempo por invocación al recorrer la matriz de selección. Esto crea poca sobrecarga para las aplicaciones de usuario que podrían tener abierto un descriptor de archivo para el sistema de ventanas y algunos para archivos abiertos, pero se convierte en un problema mayor a medida que aumenta el número de fuentes de eventos potenciales y puede obstaculizar el desarrollo de aplicaciones de servidor de muchos clientes, como en el problema C10k ; otros métodos asincrónicos pueden ser notablemente más eficientes en tales casos. Algunos Unix proporcionan llamadas específicas del sistema con mejor escalabilidad; por ejemplo, epoll
en Linux (que llena la matriz de selección de retorno solo con aquellas fuentes de eventos en las que se ha producido un evento), kqueue
en FreeBSD y puertos de eventos (y /dev/poll
) en Solaris .
El sistema Unix SVR3 proporcionaba la poll
llamada al sistema. Se podría decir que su nombre es mejor que select
, pero para los fines de este debate es esencialmente lo mismo. Los sistemas Unix SVR4 (y, por lo tanto, POSIX ) ofrecen ambas llamadas.
Disponible en BSD y POSIX Unix. La E/S se emite de forma asincrónica y, cuando se completa, se genera una señal ( interrupción ). Al igual que en la programación de bajo nivel del núcleo, las funciones disponibles para un uso seguro dentro del controlador de señales son limitadas y el flujo principal del proceso podría haberse interrumpido en casi cualquier punto, lo que daría como resultado estructuras de datos inconsistentes, tal como las ve el controlador de señales. El controlador de señales normalmente no puede emitir más E/S asincrónicas por sí mismo.
El enfoque de la señal , aunque relativamente simple de implementar dentro del sistema operativo, trae al programa de aplicación el indeseable lastre asociado con la escritura del sistema de interrupción del núcleo de un sistema operativo. Su peor característica es que cada llamada al sistema bloqueadora (sincrónica) es potencialmente interrumpible; el programador generalmente debe incorporar código de reintento en cada llamada. [ cita requerida ]
Disponible en el sistema operativo clásico Mac OS , VMS y Windows . Tiene muchas de las características del método de señal , ya que es básicamente lo mismo, aunque rara vez se lo reconoce como tal. La diferencia es que cada solicitud de E/S normalmente puede tener su propia función de finalización, mientras que el sistema de señal tiene una única devolución de llamada.
Por otro lado, un problema potencial de usar devoluciones de llamadas es que la profundidad de la pila puede crecer de manera inmanejable, ya que una cosa extremadamente común que se hace cuando se termina una E/S es programar otra. Si esto se debe satisfacer inmediatamente, la primera devolución de llamada no se "desenrolla" de la pila antes de que se invoque la siguiente. Los sistemas para evitar esto (como la programación "a mitad de camino" del nuevo trabajo) agregan complejidad y reducen el rendimiento. Sin embargo, en la práctica, esto generalmente no es un problema porque la nueva E/S generalmente regresará por sí misma tan pronto como se inicie la nueva E/S, lo que permite que la pila se "desenrolle". El problema también se puede prevenir evitando más devoluciones de llamadas, por medio de una cola, hasta que regrese la primera devolución de llamada.
Los procesos livianos (LWP) o subprocesos están disponibles en la mayoría de los sistemas operativos modernos. Son similares al método de procesos , pero con una menor sobrecarga y sin el aislamiento de datos que dificulta la coordinación de los flujos. Cada LWP o subproceso utiliza E/S sincrónicas de bloqueo tradicionales, lo que simplifica la lógica de programación; este es un paradigma común utilizado en muchos lenguajes de programación, incluidos Java y Rust. El subprocesamiento múltiple necesita utilizar mecanismos de sincronización proporcionados por el núcleo y bibliotecas seguras para subprocesos . Este método no es el más adecuado para aplicaciones de escala extremadamente grande, como servidores web, debido a la gran cantidad de subprocesos necesarios.
Este enfoque también se utiliza en el sistema de ejecución del lenguaje de programación Erlang . La máquina virtual Erlang utiliza E/S asincrónicas utilizando un pequeño grupo de solo unos pocos subprocesos o, a veces, solo un proceso, para manejar la E/S de hasta millones de procesos Erlang. El manejo de E/S en cada proceso se escribe principalmente utilizando E/S sincrónicas de bloqueo. De esta manera, el alto rendimiento de la E/S asincrónica se fusiona con la simplicidad de la E/S normal (consulte el modelo Actor ). Muchos problemas de E/S en Erlang se asignan al paso de mensajes, que se pueden procesar fácilmente utilizando la recepción selectiva incorporada.
Las fibras / corrutinas pueden considerarse un enfoque liviano similar para realizar E/S asincrónicas fuera del sistema de ejecución de Erlang, aunque no brindan exactamente las mismas garantías que los procesos de Erlang.
Disponible en Microsoft Windows , Solaris , AmigaOS , DNIX y Linux (usando io_uring , disponible en 5.1 y superiores). [1] Las solicitudes de E/S se emiten de forma asincrónica, pero las notificaciones de finalización se proporcionan a través de un mecanismo de cola de sincronización en el orden en que se completan. Generalmente asociado con una estructuración de máquina de estados del proceso principal ( programación impulsada por eventos ), que puede tener poca semejanza con un proceso que no usa E/S asincrónica o que usa una de las otras formas, lo que dificulta la reutilización del código [ cita requerida ] . No requiere mecanismos de sincronización especiales adicionales o bibliotecas seguras para subprocesos , ni los flujos textuales (código) y de tiempo (evento) están separados.
Disponible en VMS y AmigaOS (se utiliza a menudo junto con un puerto de finalización). Tiene muchas de las características del método de cola de finalización , ya que es esencialmente una cola de finalización de profundidad uno. Para simular el efecto de la "profundidad" de la cola, se requiere un indicador de evento adicional para cada evento potencial no procesado (pero completado), o se puede perder la información del evento. Esperar al próximo evento disponible en un grupo de este tipo requiere mecanismos de sincronización que pueden no escalar bien a un mayor número de eventos potencialmente paralelos.
Disponible en mainframes de IBM , Groupe Bull y Unisys . La E/S de canal está diseñada para maximizar la utilización y el rendimiento de la CPU al descargar la mayor parte de la E/S en un coprocesador. El coprocesador tiene DMA integrado, maneja las interrupciones del dispositivo, está controlado por la CPU principal y solo interrumpe a la CPU principal cuando es realmente necesario. Esta arquitectura también admite los denominados programas de canal que se ejecutan en el procesador de canal para realizar el trabajo pesado de las actividades y los protocolos de E/S.
Disponible en Windows Server 2012 y Windows 8. Optimizado para aplicaciones que procesan grandes cantidades de mensajes pequeños para lograr un mayor número de operaciones de E/S por segundo con menor inestabilidad y latencia. [2]
La gran mayoría de hardware informático de uso general se basa completamente en dos métodos para implementar E/S asincrónicas: sondeo e interrupciones. Por lo general, ambos métodos se utilizan juntos; el equilibrio depende en gran medida del diseño del hardware y de sus características de rendimiento requeridas. ( El DMA no es en sí otro método independiente, es simplemente un medio por el cual se puede realizar más trabajo por sondeo o interrupción).
Los sistemas de sondeo puro son perfectamente posibles, los microcontroladores pequeños (como los sistemas que utilizan el PIC ) suelen construirse de esta manera. Los sistemas CP/M también podrían construirse de esta manera (aunque rara vez se hizo), con o sin DMA. Además, cuando se necesita el máximo rendimiento para sólo unas pocas tareas, a expensas de otras tareas potenciales, el sondeo también puede ser apropiado, ya que la sobrecarga de tomar interrupciones puede ser indeseable. (Atender una interrupción requiere tiempo [y espacio] para guardar al menos parte del estado del procesador, junto con el tiempo necesario para reanudar la tarea interrumpida).
La mayoría de los sistemas informáticos de uso general dependen en gran medida de las interrupciones. Puede ser posible un sistema de interrupción puro, aunque normalmente también se requiere algún componente de sondeo, ya que es muy común que múltiples fuentes potenciales de interrupciones compartan una línea de señal de interrupción común, en cuyo caso se utiliza el sondeo dentro del controlador del dispositivo para resolver la fuente real. (Este tiempo de resolución también contribuye a la pérdida de rendimiento de un sistema de interrupción. A lo largo de los años se ha trabajado mucho para intentar minimizar la sobrecarga asociada con el mantenimiento de una interrupción. Los sistemas de interrupción actuales son bastante descuidados en comparación con algunos de los anteriores altamente optimizados, pero el aumento general en el rendimiento del hardware ha mitigado esto en gran medida).
También son posibles los enfoques híbridos, en los que una interrupción puede desencadenar el inicio de una ráfaga de E/S asincrónica y se utiliza el sondeo dentro de la propia ráfaga. Esta técnica es común en los controladores de dispositivos de alta velocidad, como los de red o disco, donde el tiempo perdido en volver a la tarea anterior a la interrupción es mayor que el tiempo hasta el siguiente servicio requerido. (El hardware de E/S común que se utiliza actualmente depende en gran medida de DMA y de grandes búferes de datos para compensar un sistema de interrupción de rendimiento relativamente bajo. Estos suelen utilizar el sondeo dentro de los bucles del controlador y pueden mostrar un rendimiento tremendo. Lo ideal es que los sondeos por dato siempre sean exitosos o, como máximo, se repitan una pequeña cantidad de veces).
En un momento dado, este tipo de enfoque híbrido era común en los controladores de disco y red donde no había DMA ni un almacenamiento en búfer significativo disponible. Debido a que las velocidades de transferencia deseadas eran más rápidas incluso de lo que podía tolerar el bucle mínimo de cuatro operaciones por dato (prueba de bits, bifurcación condicional a sí mismo, búsqueda y almacenamiento), el hardware a menudo se construía con generación automática de estado de espera en el dispositivo de E/S, sacando la consulta de datos listos del software y enviándola al hardware de búsqueda o almacenamiento del procesador y reduciendo el bucle programado a dos operaciones (en efecto, utilizando el propio procesador como motor de DMA). El procesador 6502 ofrecía un medio inusual para proporcionar un bucle de tres elementos por dato, ya que tenía un pin de hardware que, cuando se activaba, hacía que el bit de desbordamiento del procesador se estableciera directamente (¡obviamente, uno tendría que tener mucho cuidado en el diseño del hardware para evitar anular el bit de desbordamiento fuera del controlador del dispositivo!).
Usando sólo estas dos herramientas (sondeo e interrupciones), todas las otras formas de E/S asincrónicas analizadas anteriormente pueden sintetizarse (y de hecho lo hacen).
En un entorno como una máquina virtual Java (JVM), se puede sintetizar E/S asincrónica aunque el entorno en el que se ejecuta la JVM no lo ofrezca en absoluto. Esto se debe a la naturaleza interpretada de la JVM. La JVM puede sondear (o recibir una interrupción) periódicamente para instituir un flujo interno de cambio de control, lo que provoca la aparición de múltiples procesos simultáneos, al menos algunos de los cuales presumiblemente existen para realizar E/S asincrónicas. (Por supuesto, a nivel microscópico el paralelismo puede ser bastante burdo y exhibir algunas características no ideales, pero en la superficie parecerá ser el deseado).
Ése es, de hecho, el problema de utilizar el sondeo en cualquier forma para sintetizar una forma diferente de E/S asincrónica. Cada ciclo de CPU que es un sondeo se desperdicia y se pierde en gastos generales en lugar de cumplir una tarea deseada. Cada ciclo de CPU que no es un sondeo representa un aumento en la latencia de reacción a la E/S pendiente. Lograr un equilibrio aceptable entre estas dos fuerzas opuestas es difícil. (Éste es el motivo por el que se inventaron los sistemas de interrupción de hardware en primer lugar).
El truco para maximizar la eficiencia es minimizar la cantidad de trabajo que debe realizarse al recibir una interrupción para activar la aplicación adecuada. En segundo lugar (pero quizás no menos importante) está el método que utiliza la propia aplicación para determinar lo que debe hacer.
Los métodos de sondeo expuestos, incluidos los mecanismos de selección/sondeo, son particularmente problemáticos (para la eficiencia de la aplicación). Aunque los eventos de E/S subyacentes en los que están interesados probablemente estén impulsados por interrupciones, la interacción con este mecanismo se sondea y puede consumir una gran cantidad de tiempo en el sondeo. Esto es particularmente cierto en el caso del sondeo potencialmente a gran escala posible a través de la selección (y el sondeo). Las interrupciones se asignan muy bien a señales, funciones de devolución de llamadas, colas de finalización y marcas de eventos; estos sistemas pueden ser muy eficientes.
Los siguientes ejemplos muestran tres enfoques para leer E/S. Los objetos y funciones son abstractos.
1. Bloqueo, sincrónico:
dispositivo = IO . open () datos = dispositivo . read () # el hilo se bloqueará hasta que haya datos en el dispositivo print ( datos )
2. Bloqueo y no bloqueo, sincrónico: (aquí IO.poll()
bloquea hasta 5 segundos, pero device.read()
no lo hace)
dispositivo = IO . open () listo = False mientras no esté listo : print ( "¡No hay datos para leer!" ) listo = IO . poll ( dispositivo , IO . INPUT , 5 ) # devuelve el control si han transcurrido 5 segundos o hay datos para leer (INPUT) datos = dispositivo . read () print ( datos )
3. Sin bloqueo, asincrónico:
ios = IO . IOService () dispositivo = IO . open ( ios )def inputHandler ( data , err ): "Manejador de datos de entrada" if not err : print ( data )device . readSome ( inputHandler ) ios . loop () # esperar hasta que se hayan completado todas las operaciones y llamar a todos los controladores apropiados
Aquí está el mismo ejemplo con Async/await :
ios = IO . IOService () dispositivo = IO . open ( ios )async def task ( ): try : datos = await dispositivo.readSome ( ) print ( datos ) except Excepción : pasar ios . addTask ( task ) ios . loop () # esperar hasta que se hayan completado todas las operaciones y llamar a todos los controladores apropiados
Aquí está el ejemplo con el patrón Reactor :
dispositivo = IO . open () reactor = IO . reactor ()def inputHandler ( data ): " Manejador de datos de entrada" print ( data ) reactor.stop ( )reactor . addHandler ( inputHandler , device , IO . INPUT ) reactor . run () # ejecuta el reactor, que maneja eventos y llama a los controladores apropiados