¿Por qué los lenguajes de programación no gestionan automáticamente el problema síncrono / asíncrono?

27

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 (¿ nbOfUsersobtuvo 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é.

Cinn
fuente
58
Realmente no entiendo tu pregunta. Si "siempre espera la operación asincrónica", entonces no es una operación asincrónica, es una operación sincrónica. ¿Puedes aclarar? ¿Quizás dar una especificación del tipo de comportamiento que está buscando? Además, "qué piensas al respecto" está fuera de tema en Ingeniería de Software . Debe formular su pregunta en el contexto de un problema concreto, que tenga una respuesta única, inequívoca, canónica, objetivamente correcta.
Jörg W Mittag
44
@ JörgWMittag Me imagino un C # hipotético que implícitamente awaites Task<T>convertirlo aT
Caleth
66
Lo que propones no es factible. No depende del compilador decidir si desea esperar el resultado o tal vez disparar y olvidar. O correr en segundo plano y esperar más tarde. ¿Por qué limitarse así?
freakish
55
Sí, es una idea terrible. Simplemente use async/ en su awaitlugar, lo que hace explícitas las partes asíncronas de la ejecución.
Bergi
55
Cuando dices que dos cosas suceden simultáneamente, estás diciendo que está bien que estas cosas sucedan en cualquier orden. Si su código no tiene forma de dejar en claro qué reordenamientos no romperán las expectativas de su código, entonces no puede hacerlos concurrentes.
Rob

Respuestas:

65

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.

amon
fuente
2
No necesariamente necesita un sistema de tipos. Futuros transparentes en, por ejemplo, ECMAScript, Smalltalk, Self, Newspeak, Io, Ioke, Seph, se pueden implementar fácilmente sin el sistema tyoe o el soporte de idiomas. En Smalltalk y sus descendientes, un objeto puede cambiar su identidad de forma transparente, en ECMAScript, puede cambiar su forma de forma transparente. Eso es todo lo que necesita para hacer que Futures sea transparente, no necesita soporte de idiomas para la asincronía.
Jörg W Mittag
66
@ JörgWMittag Entiendo lo que estás diciendo y cómo podría funcionar, pero los futuros transparentes sin un sistema de tipos dificultan tener simultáneamente futuros de primera clase, ¿no? Necesitaría alguna forma de seleccionar si quiero enviar mensajes al futuro o al valor del futuro, preferiblemente algo mejor que someValue ifItIsAFuture [self| self messageIWantToSend]porque es difícil de integrar con código genérico.
amon
8
@amon "Puedo escribir mi código asíncrono como promesas y promesas son mónadas". Las mónadas no son realmente necesarias aquí. Los thunks son esencialmente solo promesas. Como casi todos los valores en Haskell están encuadrados, casi todos los valores en Haskell ya son promesas. Es por eso que puede arrojar parprácticamente cualquier lugar en código Haskell puro y obtener paralelismo de forma gratuita.
DarthFennec
2
Async / waitit me recuerda a la mónada de continuación.
Les
3
De hecho, tanto las excepciones como async / wait son instancias de efectos algebraicos .
Alex pensando de nuevo el
21

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.

cmaster
fuente
Estaba pensando en un lenguaje funcional donde las operaciones no están bloqueando, por lo que incluso si tiene una sintaxis sincrónica, un cálculo de larga duración no bloqueará el hilo
Cinn
66
@Cinn No encontré eso en la pregunta, y el ejemplo en la pregunta es Javascript, que no tiene esta característica. Sin embargo, en general es bastante difícil para un compilador encontrar oportunidades significativas para la paralelización como usted describe: la explotación significativa de una característica de este tipo requeriría que el programador piense explícitamente en lo que pone justo después de una larga llamada de latencia. Si hace que el tiempo de ejecución sea lo suficientemente inteligente como para evitar este requisito en el programador, es probable que su tiempo de ejecución consuma los ahorros de rendimiento porque necesitaría paralelizar agresivamente las llamadas de función.
cmaster
2
Todas las computadoras esperan a la misma velocidad.
Bob Jarvis - Restablece a Monica el
2
@BobJarvis Sí. Pero difieren en la cantidad de trabajo que podrían haber hecho en el tiempo de espera ...
cmaster
13

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) a T, 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.

Telastyn
fuente
1
Creo que una idea podría ser envolver todo en funciones asíncronas, las tareas sincrónicas simplemente se resolverían de inmediato y obtendríamos un tipo único para manejar (Edición: @amon explicó por qué es una mala idea ...)
Cinn
8
¿Puedes dar algunos ejemplos de " Some do ", por favor?
Bergi
2
La programación asincrónica no es de ninguna manera nueva, es solo que hoy en día la gente tiene que lidiar con ella con más frecuencia.
Cubic
1
@Cubic: por lo que sé, es una característica del lenguaje. Antes era solo (incómodo) las funciones de userland.
Telastyn
12

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>. Esto Future<Int>tiene los mismos métodos que Int, excepto con diferentes implementaciones. Future<Int>El +método no agrega otro número y devuelve el resultado, devuelve uno nuevo Future<Int>que envuelve el cálculo. Y así sucesivamente y así sucesivamente. Los métodos que no pueden implementarse de manera sensata devolviendo a Future<Int>, en su lugar, automáticamente generarán awaitel resultado y luego llamarán self become: result., lo que hará que el objeto que se está ejecutando actualmente ( selfes decir, el Future<Int>) se convierta literalmente en el resultobjeto, es decir, a partir de ahora, la referencia de objeto que solía ser a Future<Int>es ahora un en Inttodas partes, completamente transparente para el cliente.

No se necesitan características especiales de lenguaje relacionadas con la asincronía.

Jörg W Mittag
fuente
Ok, pero que no tiene problemas si ambos Future<T>y Tcomparten alguna de las interfaces comunes y utilizar la funcionalidad de la interfaz. ¿Debería ser becomeel 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.
amon
Entiendo que no agrega ninguna característica, lo que pasa es que tenemos diferentes sintaxis para escribir cálculos de resolución inmediata y cálculos de ejecución prolongada, y luego usaríamos los resultados de la misma manera para otros fines. Me preguntaba si podríamos tener una sintaxis que maneje de manera transparente los dos, haciéndolo más legible y así el programador no tenga que manejarlo. Como hacer a + b, ambos enteros, no importa si ayb están disponibles de inmediato o más tarde, solo escribimos a + b(haciendo posible Int + Future<Int>)
Cinn
@Cinn: Sí, puedes hacerlo con Transparent Futures, y no necesitas ninguna función de lenguaje especial para hacerlo. Puede implementarlo utilizando las funciones ya existentes en, por ejemplo, Smalltalk, Self, Newspeak, Us, Korz, Io, Ioke, Seph, ECMAScript y, aparentemente, como acabo de leer, Python.
Jörg W Mittag
3
@amon: La idea de Futuros Transparentes es que no sabes que es un futuro. Desde su punto de vista, no hay una interfaz común entre Future<T>y Tporque desde su punto de vista, no hayFuture<T> , solo a T. 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.
Jörg W Mittag
3
@ Jörg Parece que sería problemático en todo menos en lenguajes funcionales, ya que no tiene forma de saber cuándo se ejecuta el código en ese modelo. Eso generalmente funciona bien, por ejemplo, en Haskell, pero no puedo ver cómo funcionaría esto en más lenguajes de procedimiento (e incluso en Haskell, si te importa el rendimiento, a veces tienes que forzar una ejecución y comprender el modelo subyacente). Una idea interesante, sin embargo.
Voo
7

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:

  1. 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:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    Tres meses después, un jugador descubre que al ser asesinado y desconectarse precisamente cuando se attacker.addInventoryItemsestá ejecutando, victim.removeInventoryItemsfallará, 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.

  2. 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:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    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:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. 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.

usuario253751
fuente
3

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 Promises 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 o CompletableFutures 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 o forkIOsoltando 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.

Louis Jackman
fuente
1
¡Esta fue una respuesta realmente interesante! Pero no estoy seguro de entender su distinción entre código sincrónico "sincrónico" y "asincrónico". Para mí, el código sincrónico sin bloqueo significa algo así como una función de C como 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?
amon
1
Utilizo "asincrónico" y "sincrónico" para analizar el modelo de programación expuesto al desarrollador y "bloquear" y "no bloquear" para analizar el bloqueo de un hilo del núcleo durante el cual no puede hacer nada útil, incluso si hay otros cálculos que deben realizarse y hay un procesador lógico adicional que puede usar. Bueno, una gorutina puede esperar un resultado sin bloquear el hilo subyacente, pero otra gorutina puede comunicarse con ella a través de un canal si lo desea. Sin embargo, la gorutina no necesita usar un canal directamente para esperar una lectura de socket sin bloqueo.
Louis Jackman
Hmm ok, entiendo tu distinción ahora. Mientras que estoy más preocupado por la gestión del flujo de datos y control entre las rutinas, usted está más preocupado por nunca bloquear el hilo principal del núcleo. No estoy seguro de que Go o Haskell tengan alguna ventaja sobre C ++ o Java a este respecto, ya que también pueden iniciar hilos de fondo, para hacerlo solo se necesita un poco más de código.
amon
@LouisJackman podría elaborar un poco sobre su última declaración sobre el bloqueo asíncrono para la programación del sistema. ¿Cuáles son las ventajas del enfoque asincrónico sin bloqueo?
Sunprophit
@sunprophit El bloqueo no asíncrono es solo una transformación del compilador (generalmente asíncrono / espera), mientras que el bloqueo sincrónico requiere soporte de tiempo de ejecución como una combinación de manipulación compleja de la pila, insertando puntos de rendimiento en llamadas a funciones (que pueden colisionar con la alineación), rastrear " reducciones "(que requieren una máquina virtual como BEAM), etc. Al igual que la recolección de basura, está compensando menos complejidad de tiempo de ejecución para facilitar su uso y robustez. Los lenguajes de sistemas como C, C ++ y Rust evitan características de tiempo de ejecución más grandes como esta debido a sus dominios específicos, por lo que el bloqueo asíncrono tiene más sentido allí.
Louis Jackman
2

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.

monocelda
fuente
2

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.

Mikey Wetzel
fuente
1

El problema que estás describiendo es doble.

  • El programa que está escribiendo debe comportarse de manera asíncrona en su conjunto cuando se ve desde el exterior .
  • Debería no ser visibles en el sitio de la llamada si una llamada a la función da potencialmente hasta el control o no.

Hay un par de maneras de lograr esto, pero básicamente se reducen a

  1. tener múltiples hilos (en algún nivel de abstracción)
  2. teniendo múltiples tipos de funciones a nivel de lenguaje, todas las cuales se llaman así 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 awaitno 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 hacer awaituna 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:

((x) => {return x + x;})

Eliminación de la función sincrónica:

f(4)

((x) => {return x + x;})(4)

Puede contrastar esto con la introducción y eliminación de funciones asincrónicas.

Introducción a la función asincrónica

(async (x) => {return x + x;})

Eliminación de funciones asincrónicas (nota: solo válido dentro de una asyncfunción)

await (async (x) => {return x + x;})(4)

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

> (async (x) => {return x + x;})(4)
Promise { 8 }

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.

Gregory Nisbet
fuente
1

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
Casi nunca utilicé el lenguaje Go, pero parece que declaras explícitamente go ..., por lo que se parece a un await ...no.
Cinn
1
@Cinn En realidad, no. Puede colocar cualquier llamada como una rutina en su propia fibra / hilo verde con 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.
Deduplicador
2
Si bien las goroutinas son una especie de concurrencia, no las pondría en el mismo cubo que async / wait: no corutinas cooperativas sino automáticamente (¡y de manera preventiva!) Hilos verdes programados. Pero esto tampoco hace que la espera sea automática: el equivalente de Go awaites leer desde un canal <- ch.
amon
@amon Hasta donde yo sé, las goroutines se programan de manera cooperativa en subprocesos nativos (normalmente lo suficiente como para maximizar el verdadero paralelismo de hardware) en el tiempo de ejecución, y el SO las programa de forma preventiva.
Deduplicador
El OP solicitó "poder escribir código asincrónico de forma síncrona". Como ha mencionado, con goroutines y el tiempo de ejecución de go, puede hacer exactamente eso. No tiene que preocuparse por los detalles del subproceso, solo escriba lecturas y escrituras de bloqueo, como si el código fuera síncrono, y sus otras rutinas, si las hubiera, seguirán ejecutándose. Tampoco tiene que "esperar" o leer un canal para obtener este beneficio. Por lo tanto, creo que Go es un lenguaje de programación que cumple con los deseos del OP más de cerca.
1

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:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

Si bar()es una función asíncrona, entonces es posible obj.xque 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:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

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ás obj.xadelante 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.

j_kubik
fuente
0

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:

const nbOfUsers = getNbOfUsers();

Tiene la garantía de que nada más se ejecutará mientras tanto. No hay necesidad de cerraduras ni nada similar.

Sin embargo, si getNbOfUserses asíncrono, entonces:

const nbOfUsers = await getNbOfUsers();

significa que mientras se getNbOfUsersejecuta, 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.

jcaron
fuente
Tiene razón, mi segundo código en la pregunta no es válido como si 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.
Cinn
@Cinn ese no es mi punto. Mi punto es que el flujo de ejecución puede llegar a otras partes de su código durante la ejecución de la llamada asincrónica, mientras que no es posible para una llamada sincrónica. Sería como tener varios subprocesos en ejecución pero no ser consciente de ello. Esto puede terminar en grandes problemas (que generalmente son difíciles de detectar y reproducir).
jcaron
-4

Está disponible en C ++ como std::asyncdesde C ++ 11.

La función de plantilla asíncrona ejecuta la función f de forma asincrónica (potencialmente en un subproceso separado que puede ser parte de un grupo de subprocesos) y devuelve un std :: future que finalmente contendrá el resultado de esa llamada de función.

Y con C ++ 20 se pueden usar corutinas:

Robert Andrzejuk
fuente
55
Esto no parece responder la pregunta. De acuerdo con su enlace: "¿Qué nos da el Coroutines TS? Tres nuevas palabras clave de lenguaje: co_await, co_yield y co_return" ... Pero la pregunta es ¿por qué necesitamos una palabra clave await(o co_awaiten este caso) en primer lugar?
Arturo Torres Sánchez