No he encontrado muchos recursos sobre esto: me preguntaba si es posible / una buena idea poder escribir código asincrónico de forma síncrona.
Por ejemplo, aquí hay un código JavaScript que recupera el número de usuarios almacenados en una base de datos (una operación asincrónica):
getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });
Sería bueno poder escribir algo como esto:
const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);
Y así, el compilador se encargaría automáticamente de esperar la respuesta y luego ejecutarlo console.log
. Siempre esperará a que se completen las operaciones asincrónicas antes de que los resultados tengan que usarse en otro lugar. Haríamos mucho menos uso de las promesas de devolución de llamada, asíncrono / espera o lo que sea, y nunca tendríamos que preocuparnos si el resultado de una operación está disponible de inmediato o no.
Los errores aún serían manejables (¿ nbOfUsers
obtuvo un número entero o un error?) Usando try / catch, o algo así como opcionales como en el lenguaje Swift .
¿Es posible? Puede ser una idea terrible / una utopía ... No lo sé.
await
esTask<T>
convertirlo aT
async
/ en suawait
lugar, lo que hace explícitas las partes asíncronas de la ejecución.Respuestas:
Async / await es exactamente esa gestión automatizada que usted propone, aunque con dos palabras clave adicionales. ¿Porque son importantes? ¿Aparte de la compatibilidad con versiones anteriores?
Sin puntos explícitos donde se pueda suspender y reanudar una rutina, necesitaríamos un sistema de tipos para detectar dónde se debe esperar un valor esperado. Muchos lenguajes de programación no tienen ese tipo de sistema.
Al hacer explícito la espera de un valor, también podemos pasar valores esperables como objetos de primera clase: promesas. Esto puede ser súper útil al escribir código de orden superior.
El código asíncrono tiene efectos muy profundos para el modelo de ejecución de un lenguaje, similar a la ausencia o presencia de excepciones en el lenguaje. En particular, una función asincrónica solo puede ser esperada por funciones asincrónicas. ¡Esto afecta a todas las funciones de llamada! Pero, ¿qué sucede si cambiamos una función de no asíncrona a asíncrona al final de esta cadena de dependencia? Esto sería un cambio incompatible con versiones anteriores ... a menos que todas las funciones sean asíncronas y todas las llamadas de función se esperen de manera predeterminada.
Y eso es muy indeseable porque tiene muy malas implicaciones de rendimiento. No podría simplemente devolver valores baratos. Cada llamada a la función sería mucho más costosa
Async es genial, pero algún tipo de async implícito no funcionará en realidad.
Los lenguajes funcionales puros como Haskell tienen una especie de trampilla de escape porque el orden de ejecución es en gran medida no especificado y no observable. O redactado de manera diferente: cualquier orden específico de operaciones debe codificarse explícitamente. Eso puede ser bastante engorroso para los programas del mundo real, especialmente aquellos programas pesados de E / S para los que el código asíncrono es muy adecuado.
fuente
someValue ifItIsAFuture [self| self messageIWantToSend]
porque es difícil de integrar con código genérico.par
prácticamente cualquier lugar en código Haskell puro y obtener paralelismo de forma gratuita.Lo que le falta es el propósito de las operaciones asíncronas: ¡le permiten utilizar su tiempo de espera!
Si convierte una operación asíncrona, como solicitar algún recurso de un servidor, en una operación síncrona al esperar implícita e inmediatamente la respuesta, su hilo no puede hacer nada más con el tiempo de espera . Si el servidor tarda 10 milisegundos en responder, se desperdician unos 30 millones de ciclos de CPU. La latencia de la respuesta se convierte en el tiempo de ejecución de la solicitud.
La única razón por la cual los programadores inventaron las operaciones asíncronas es para ocultar la latencia de las tareas inherentemente largas detrás de otros cálculos útiles . Si puede llenar el tiempo de espera con trabajo útil, ese es el tiempo de CPU ahorrado. Si no puede, bueno, nada se pierde por la operación asincrónica.
Por lo tanto, recomiendo adoptar las operaciones asíncronas que le proporcionan sus idiomas. Están ahí para ahorrarle tiempo.
fuente
Algunos lo hacen.
No son convencionales (todavía) porque el asíncrono es una característica relativamente nueva por la que recién ahora hemos tenido una buena idea, incluso si es una buena característica, o cómo presentarla a los programadores de una manera amigable / usable / expresivo / etc. Las funciones asíncronas existentes están en gran medida atornilladas a los idiomas existentes, que requieren un enfoque de diseño un poco diferente.
Dicho esto, claramente no es una buena idea hacerlo en todas partes. Un error común es hacer llamadas asíncronas en un bucle, serializando efectivamente su ejecución. Tener llamadas asíncronas implícitas puede ocultar ese tipo de error. Además, si admite la coerción implícita de un
Task<T>
(o el equivalente de su idioma) aT
, eso puede agregar un poco de complejidad / costo a su typechecker e informes de errores cuando no está claro cuál de los dos realmente quería el programador.Pero esos no son problemas insuperables. Si quisieras apoyar ese comportamiento, casi seguro que podrías, aunque habría compensaciones.
fuente
Hay idiomas que hacen esto. Pero, en realidad no hay mucha necesidad, ya que se puede lograr fácilmente con las funciones de lenguaje existentes.
Mientras tenga alguna forma de expresar la asincronía, puede implementar Futures or Promises simplemente como una función de biblioteca, no necesita ninguna función especial de lenguaje. Y siempre que tenga algo de expresión de Proxies transparentes , puede juntar las dos características y tener Futuros transparentes .
Por ejemplo, en Smalltalk y sus descendientes, un objeto puede cambiar su identidad, literalmente puede "convertirse" en un objeto diferente (y, de hecho, el método que hace esto se llama
Object>>become:
).Imagine un cálculo de larga duración que devuelve a
Future<Int>
. EstoFuture<Int>
tiene los mismos métodos queInt
, excepto con diferentes implementaciones.Future<Int>
El+
método no agrega otro número y devuelve el resultado, devuelve uno nuevoFuture<Int>
que envuelve el cálculo. Y así sucesivamente y así sucesivamente. Los métodos que no pueden implementarse de manera sensata devolviendo aFuture<Int>
, en su lugar, automáticamente generaránawait
el resultado y luego llamaránself become: result.
, lo que hará que el objeto que se está ejecutando actualmente (self
es decir, elFuture<Int>
) se convierta literalmente en elresult
objeto, es decir, a partir de ahora, la referencia de objeto que solía ser aFuture<Int>
es ahora un enInt
todas partes, completamente transparente para el cliente.No se necesitan características especiales de lenguaje relacionadas con la asincronía.
fuente
Future<T>
yT
comparten alguna de las interfaces comunes y utilizar la funcionalidad de la interfaz. ¿Debería serbecome
el resultado y luego usar la funcionalidad, o no? Estoy pensando en cosas como un operador de igualdad o una representación de depuración de cadenas.a + b
, ambos enteros, no importa si ayb están disponibles de inmediato o más tarde, solo escribimosa + b
(haciendo posibleInt + Future<Int>
)Future<T>
yT
porque desde su punto de vista, no hayFuture<T>
, solo aT
. Ahora, por supuesto, hay muchos desafíos de ingeniería sobre cómo hacer esto eficiente, qué operaciones deberían ser bloqueadas frente a no bloqueadas, etc., pero eso es realmente independiente de si lo haces como un lenguaje o como una función de biblioteca. La transparencia fue un requisito estipulado por el OP en la pregunta, no voy a argumentar que es difícil y podría no tener sentido.Lo hacen (bueno, la mayoría de ellos). La característica que está buscando se llama hilos .
Sin embargo, los hilos tienen sus propios problemas:
Debido a que el código se puede suspender en cualquier momento , nunca se puede suponer que las cosas no cambiarán "por sí mismas". Al programar con hilos, pierdes mucho tiempo pensando en cómo tu programa debe lidiar con las cosas que cambian.
Imagine que un servidor de juegos está procesando el ataque de un jugador contra otro jugador. Algo como esto:
Tres meses después, un jugador descubre que al ser asesinado y desconectarse precisamente cuando se
attacker.addInventoryItems
está ejecutando,victim.removeInventoryItems
fallará, puede quedarse con sus objetos y el atacante también obtiene una copia de sus objetos. Lo hace varias veces, creando un millón de toneladas de oro de la nada y destruyendo la economía del juego.Alternativamente, el atacante puede cerrar sesión mientras el juego está enviando un mensaje a la víctima, y no recibirá una etiqueta de "asesino" sobre su cabeza, por lo que su próxima víctima no huirá de él.
Debido a que el código se puede suspender en cualquier momento , debe usar bloqueos en todas partes al manipular estructuras de datos. Di un ejemplo anterior que tiene consecuencias obvias en un juego, pero puede ser más sutil. Considere agregar un elemento al comienzo de una lista vinculada:
Esto no es un problema si dice que los hilos solo se pueden suspender cuando están haciendo E / S, y no en ningún momento. Pero estoy seguro de que puede imaginar una situación en la que hay una operación de E / S, como el registro:
Debido a que el código puede suspenderse en cualquier momento , podría haber mucho estado para guardar. El sistema se ocupa de esto dando a cada hilo una pila completamente separada. Pero la pila es bastante grande, por lo que no puede tener más de 2000 hilos en un programa de 32 bits. O podría reducir el tamaño de la pila, a riesgo de hacerlo demasiado pequeño.
fuente
Aquí encuentro muchas de las respuestas engañosas, porque si bien la pregunta era literalmente sobre programación asincrónica y no IO sin bloqueo, no creo que podamos discutir una sin discutir la otra en este caso en particular.
Si bien la programación asincrónica es inherentemente, bueno, asincrónica, la razón de ser de la programación asincrónica es principalmente evitar el bloqueo de hilos del núcleo. Node.js usa la asincronía a través de devoluciones de llamada o
Promise
s para permitir que las operaciones de bloqueo se envíen desde un bucle de eventos y Netty en Java usa la asincronía a través de devoluciones de llamada oCompletableFuture
s para hacer algo similar.Sin embargo, el código sin bloqueo no requiere asincronía . Depende de cuánto esté dispuesto a hacer por usted su lenguaje de programación y su tiempo de ejecución.
Go, Erlang y Haskell / GHC pueden manejar esto por usted. Puede escribir algo así
var response = http.get('example.com/test')
y hacer que libere un hilo del kernel detrás de escena mientras espera una respuesta. Esto se hace mediante goroutines, procesos de Erlang oforkIO
soltando hilos del kernel detrás de escena cuando se bloquea, lo que le permite hacer otras cosas mientras espera una respuesta.Es cierto que el lenguaje realmente no puede manejar la asincronía por ti, pero algunas abstracciones te permiten llegar más lejos que otras, por ejemplo, continuaciones no delimitadas o rutinas asimétricas. Sin embargo, la causa principal del código asincrónico, el bloqueo de las llamadas al sistema, se puede abstraer absolutamente del desarrollador.
Node.js y Java admiten código sin bloqueo asíncrono , mientras que Go y Erlang admiten código sin bloqueo sincrónico . Ambos son enfoques válidos con diferentes compensaciones.
Mi argumento bastante subjetivo es que aquellos que argumentan en contra de los tiempos de ejecución que gestionan el no bloqueo en nombre del desarrollador son como aquellos que argumentan en contra de la recolección de basura a principios de los años noventa. Sí, incurre en un costo (en este caso principalmente más memoria), pero facilita el desarrollo y la depuración, y hace que las bases de código sean más robustas.
Yo personalmente argumentaría que el código asincrónico sin bloqueo debería reservarse para la programación de sistemas en el futuro y que las pilas de tecnología más modernas deberían migrar a tiempos de ejecución sincrónicos sincrónicos para el desarrollo de aplicaciones.
fuente
waitpid(..., WNOHANG)
esa falla si tuviera que bloquear. ¿O "sincrónico" aquí significa "no hay devoluciones de llamada / máquinas de estado / bucles de eventos visibles por el programador"? Pero para su ejemplo de Go, todavía tengo que esperar explícitamente el resultado de una rutina al leer desde un canal, ¿no? ¿Cómo es esto menos asíncrono que asíncrono / espera en JS / C # / Python?Si te estoy leyendo bien, estás pidiendo un modelo de programación síncrona, pero una implementación de alto rendimiento. Si eso es correcto, entonces eso ya está disponible para nosotros en forma de hilos verdes o procesos de, por ejemplo, Erlang o Haskell. Entonces, sí, es una idea excelente, pero la adaptación a los idiomas existentes no siempre puede ser tan fácil como quisiera.
fuente
Aprecio la pregunta y encuentro que la mayoría de las respuestas son meramente defensivas del status quo. En el espectro de los idiomas de bajo a alto nivel, hemos estado atrapados en una rutina por algún tiempo. El siguiente nivel superior será claramente un lenguaje que se centre menos en la sintaxis (la necesidad de palabras clave explícitas como esperar y asincrónica) y mucho más sobre la intención. (Crédito obvio para Charles Simonyi, pero pensando en 2019 y el futuro).
Si le dije a un programador, escriba un código que simplemente obtenga un valor de una base de datos, puede suponer con seguridad que quiero decir "y, por cierto, no cuelgue la interfaz de usuario" y "no introduzca otras consideraciones que oculten errores difíciles de encontrar ". Los programadores del futuro, con una próxima generación de lenguajes y herramientas, ciertamente podrán escribir código que simplemente obtenga un valor en una línea de código y vaya desde allí.
El idioma de más alto nivel sería hablar inglés y confiar en la competencia del encargado de la tarea para saber lo que realmente quiere que se haga. (Piense en la computadora en Star Trek, o preguntándole algo a Alexa.) Estamos lejos de eso, pero cada vez más cerca, y mi expectativa es que el lenguaje / compilador podría generar más código robusto e intencionado sin ir tan lejos como para necesitando IA.
Por un lado, hay nuevos lenguajes visuales, como Scratch, que hacen esto y no están empantanados con todos los tecnicismos sintácticos. Ciertamente, hay mucho trabajo detrás de escena para que el programador no tenga que preocuparse por eso. Dicho esto, no estoy escribiendo software de clase empresarial en Scratch, por lo que, como usted, tengo la misma expectativa de que es hora de que los lenguajes de programación maduros administren automáticamente el problema síncrono / asíncrono.
fuente
El problema que estás describiendo es doble.
Hay un par de maneras de lograr esto, pero básicamente se reducen a
foo(4, 7, bar, quux)
.Para (1), estoy agrupando bifurcando y ejecutando múltiples procesos, generando múltiples hilos de kernel e implementaciones de hilos verdes que programan hilos de nivel de tiempo de ejecución de lenguaje en hilos de kernel. Desde la perspectiva del problema, son lo mismo. En este mundo, ninguna función abandona o pierde el control desde la perspectiva de su hilo . El hilo en sí a veces no tiene control y a veces no se está ejecutando, pero no cedes el control de tu propio hilo en este mundo. Un sistema que se ajuste a este modelo puede o no tener la capacidad de generar nuevos hilos o unirse a los hilos existentes. Un sistema que se ajuste a este modelo puede o no tener la capacidad de duplicar un hilo como el de Unix
fork
.(2) es interesante. Para hacer justicia, necesitamos hablar sobre las formas de introducción y eliminación.
Voy a mostrar por qué lo implícito
await
no se puede agregar a un lenguaje como Javascript de una manera compatible con versiones anteriores. La idea básica es que al exponer las promesas al usuario y distinguir entre contextos síncronos y asíncronos, Javascript ha filtrado un detalle de implementación que evita el manejo uniforme de las funciones síncronas y asíncronas. También está el hecho de que no puedes hacerawait
una promesa fuera de un cuerpo de función asíncrona. Estas opciones de diseño son incompatibles con "hacer que la asincronía sea invisible para la persona que llama".Puede introducir una función síncrona utilizando una lambda y eliminarla con una llamada a la función.
Introducción a la función síncrona:
Eliminación de la función sincrónica:
Puede contrastar esto con la introducción y eliminación de funciones asincrónicas.
Introducción a la función asincrónica
Eliminación de funciones asincrónicas (nota: solo válido dentro de una
async
función)El problema fundamental aquí es que una función asincrónica también es una función síncrona que produce un objeto de promesa .
Aquí hay un ejemplo de llamar a una función asincrónica sincrónicamente en la respuesta node.js
Hipotéticamente puede tener un idioma, incluso uno de tipo dinámico, donde la diferencia entre las llamadas a funciones asíncronas y síncronas no es visible en el sitio de la llamada y posiblemente no sea visible en el sitio de definición.
Es posible tomar un lenguaje como ese y reducirlo a Javascript, solo tendría que hacer que todas las funciones sean asincrónicas de manera efectiva.
fuente
Con las rutinas de lenguaje Go y el tiempo de ejecución del lenguaje Go, puede escribir todo el código como si fuera sincrónica. Si una operación se bloquea en una goroutina, la ejecución continúa en otras goroutinas. Y con canales puede comunicarse fácilmente entre goroutines. Esto es a menudo más fácil que las devoluciones de llamada como en Javascript o async / await en otros idiomas. Consulte https://tour.golang.org/concurrency/1 para ver algunos ejemplos y una explicación.
Además, no tengo experiencia personal con él, pero escuché que Erlang tiene instalaciones similares.
Entonces, sí, hay lenguajes de programación como Go y Erlang, que resuelven el problema síncrono / asíncrono, pero desafortunadamente aún no son muy populares. A medida que esos idiomas crecen en popularidad, quizás las instalaciones que brindan se implementarán también en otros idiomas.
fuente
go ...
, por lo que se parece a unawait ...
no.go
. Y casi cualquier llamada que pueda bloquearse se realiza de forma asincrónica por el tiempo de ejecución, que mientras tanto cambia a una rutina diferente (multitarea cooperativa). Esperas esperando un mensaje.await
es leer desde un canal<- ch
.Hay un aspecto muy importante que aún no se ha planteado: la reentrada. Si tiene algún otro código (es decir: bucle de eventos) que se ejecuta durante la llamada asincrónica (y si no lo tiene, ¿por qué necesita async?), Entonces el código puede afectar el estado del programa. No puede ocultar las llamadas asíncronas de la persona que llama porque la persona que llama puede depender de partes del estado del programa para que no se vean afectadas durante la duración de su llamada de función. Ejemplo:
Si
bar()
es una función asíncrona, entonces es posibleobj.x
que cambie durante su ejecución. Esto sería bastante inesperado sin ninguna pista de que la barra es asíncrona y que el efecto es posible. La única alternativa sería sospechar que cada función / método posible sea asíncrono y volver a buscar y volver a verificar cualquier estado no local después de cada llamada a la función. Esto es propenso a errores sutiles y puede que ni siquiera sea posible si parte del estado no local se obtiene a través de funciones. Debido a eso, el programador necesita saber cuáles de las funciones tienen el potencial de alterar el estado del programa de maneras inesperadas:Ahora es claramente visible que se
bar()
trata de una función asíncrona, y la forma correcta de manejarla es volver a verificar el valor esperado de másobj.x
adelante y tratar con cualquier cambio que pueda haber ocurrido.Como ya se señaló en otras respuestas, los lenguajes funcionales puros como Haskell pueden escapar de ese efecto por completo al evitar la necesidad de cualquier estado compartido / global. No tengo mucha experiencia con lenguajes funcionales, por lo que probablemente estoy predispuesto en su contra, pero no creo que la falta del estado global sea una ventaja cuando escribo aplicaciones más grandes.
fuente
En el caso de Javascript, que utilizó en su pregunta, hay un punto importante a tener en cuenta: Javascript es de un solo subproceso y el orden de ejecución está garantizado siempre que no haya llamadas asíncronas.
Entonces, si tienes una secuencia como la tuya:
Tiene la garantía de que nada más se ejecutará mientras tanto. No hay necesidad de cerraduras ni nada similar.
Sin embargo, si
getNbOfUsers
es asíncrono, entonces:significa que mientras se
getNbOfUsers
ejecuta, los rendimientos de ejecución y otros códigos pueden ejecutarse en el medio. Esto a su vez puede requerir un cierto bloqueo, dependiendo de lo que esté haciendo.Por lo tanto, es una buena idea saber cuándo una llamada es asíncrona y cuándo no, ya que en algunas situaciones deberá tomar precauciones adicionales que no necesitaría si la llamada fuera sincrónica.
fuente
getNbOfUsers()
devolviera una Promesa. Pero ese es exactamente el punto de mi pregunta, ¿por qué necesitamos escribirlo explícitamente como asíncrono? El compilador podría detectarlo y manejarlo automáticamente de una manera diferente.Está disponible en C ++ como
std::async
desde C ++ 11.Y con C ++ 20 se pueden usar corutinas:
fuente
await
(oco_await
en este caso) en primer lugar?