explained - create async method c#
¿Cómo se relaciona C#async/await con construcciones más generales, por ejemplo, flujos de trabajo F#o mónadas? (2)
El diseño del lenguaje C # siempre se ha orientado (históricamente) a resolver problemas específicos en lugar de buscar los problemas generales subyacentes: ver, por ejemplo, http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx para "IEnumerable vs. coroutines":
Podríamos haberlo hecho mucho más general. Nuestros bloques de iteradores se pueden ver como un tipo de corrutina débil. Podríamos haber optado por implementar corutinas completas y simplemente hacer bloques de iterador en un caso especial de corutinas. Y, por supuesto, las corutinas son a su vez menos generales que las continuaciones de primera clase; podríamos haber implementado continuaciones, corutinas implementadas en términos de continuaciones e iteradores en términos de corutinas.
o http://blogs.msdn.com/b/wesdyer/archive/2008/01/11/the-marvels-of-monads.aspx para SelectMany como sustituto de (algún tipo de) mónadas:
El sistema de tipo C # no es lo suficientemente potente como para crear una abstracción generalizada para las mónadas, que fue el principal motivador para crear métodos de extensión y el "patrón de consulta"
No quiero preguntar por qué ha sido así (ya se han dado muchas buenas respuestas, especialmente en el blog de Eric, que puede aplicarse a todas estas decisiones de diseño: desde el rendimiento hasta una mayor complejidad, tanto para el compilador como para el programador).
Lo que trato de entender es a qué "construcción general" se refieren las palabras clave async / await (mi mejor estimación es la mónada de continuación; después de todo, F # async se implementa mediante flujos de trabajo, que a mi entender es una mónada de continuación), y cómo se relacionan con esto (¿cómo se diferencian ?, ¿qué falta ?, ¿por qué hay una brecha, si hay alguna?)
Estoy buscando una respuesta similar al artículo de Eric Lippert que vinculé, pero relacionado con async / await en lugar de IEnumerable / yield.
Editar : además de las excelentes respuestas, algunos enlaces útiles a preguntas relacionadas y publicaciones de blog donde se sugirieron, estoy editando mi pregunta para enumerarlas:
El modelo de programación asíncrono en C # es muy similar a los flujos de trabajo asincrónicos en F #, que son una instancia del patrón de mónada general. De hecho, la sintaxis del iterador C # también es una instancia de este patrón, aunque necesita alguna estructura adicional, por lo que no es solo una mónada simple .
Explicar esto está más allá del alcance de una sola respuesta SO, pero permítanme explicar las ideas clave.
Operaciones monádicas La asincrónica C # consiste esencialmente en dos operaciones primitivas. Puede await
un cálculo asincrónico y puede return
el resultado de un cálculo asincrónico (en el primer caso, esto se hace usando una nueva palabra clave, mientras que en el segundo caso, estamos reutilizando una palabra clave que ya está en el idioma )
Si estuviera siguiendo el patrón general ( mónada ), entonces traduciría el código asíncrono en llamadas a las dos operaciones siguientes:
Task<R> Bind<T, R>(Task<T> computation, Func<T, Task<R>> continuation);
Task<T> Return<T>(T value);
Ambos se pueden implementar con bastante facilidad utilizando la API de tareas estándar: la primera es esencialmente una combinación de ContinueWith
y Unwrap
y la segunda simplemente crea una tarea que devuelve el valor de inmediato. Voy a usar las dos operaciones anteriores porque capturan mejor la idea.
Traducción. La clave es traducir el código asíncrono al código normal que usa las operaciones anteriores.
Veamos un caso cuando avaitamos una expresión e
y luego asignamos el resultado a una variable x
y evaluamos el body
expresión (o bloque de enunciados) (en C #, puede esperar la expresión dentro, pero siempre puede traducir eso al código que primero asigna el resultado a una variable):
[| var x = await e; body |]
= Bind(e, x => [| body |])
Estoy usando una notación que es bastante común en los lenguajes de programación. El significado de [| e |] = (...)
[| e |] = (...)
es que traducimos la expresión e
(en "corchetes semánticos") a alguna otra expresión (...)
.
En el caso anterior, cuando tiene una expresión con await e
, se traduce a la operación Bind
y el cuerpo (el resto del código sigue en espera) se inserta en una función lambda que se pasa como un segundo parámetro para Bind
.
¡Aquí es donde sucede lo interesante! En lugar de evaluar el resto del código de forma inmediata (o bloquear un hilo mientras se espera), la operación de Bind
puede ejecutar la operación asincrónica (representada por e
que es del tipo Task<T>
) y, cuando la operación finaliza, finalmente puede invocar la función lambda (continuación) para ejecutar el resto del cuerpo.
La idea de la traducción es que convierte el código ordinario que devuelve algún tipo R
en una tarea que devuelve el valor de forma asíncrona, es decir, la Task<R>
. En la ecuación anterior, el tipo de devolución de Bind
es, de hecho, una tarea. Esta es también la razón por la cual necesitamos traducir el return
:
[| return e |]
= Return(e)
Esto es bastante simple: cuando tiene un valor resultante y desea devolverlo, simplemente lo ajusta en una tarea que se completa inmediatamente. Esto puede sonar inútil, pero recuerde que debemos devolver una Task
porque la operación de Bind
(y toda nuestra traducción) lo requiere.
Ejemplo más grande. Si miras un ejemplo más grande que contiene múltiples await
:
var x = await AsyncOperation();
return await x.AnotherAsyncOperation();
El código se traduciría a algo como esto:
Bind(AsyncOperation(), x =>
Bind(x.AnotherAsyncOperation(), temp =>
Return(temp));
El truco clave es que cada Bind
convierte el resto del código en una continuación (lo que significa que puede evaluarse cuando se completa una operación asincrónica).
Continuación mónada En C #, el mecanismo asincrónico no se implementa realmente utilizando la traducción anterior. La razón es que si te enfocas solo en la sincronización, puedes hacer una compilación más eficiente (que es lo que C # hace) y producir una máquina de estados directamente. Sin embargo, lo anterior es más o menos cómo funcionan los flujos de trabajo asíncronos en F #. Esta es también la fuente de flexibilidad adicional en F #: puede definir su propio Bind
y Return
para otras cosas, como operaciones para trabajar con secuencias, seguimiento de registro, creación de cálculos reanudables o incluso la combinación de cálculos asincrónicos con secuencias (la secuencia asíncrona puede generar resultados múltiples, pero también pueden esperar).
La implementación F # se basa en la mónada de continuación, lo que significa que la Task<T>
(en realidad, Async<T>
) en F # se define más o menos así:
Async<T> = Action<Action<T>>
Es decir, un cálculo asincrónico es una acción. Cuando le da Action<T>
(una continuación) como argumento, comenzará a trabajar y luego, cuando termine, invoca esta acción que usted especificó. Si buscas mónadas de continuación, entonces estoy seguro de que puedes encontrar una mejor explicación de esto tanto en C # como en F #, así que me detendré aquí ...
La respuesta de Tomás es muy buena. Para agregar algunas cosas más:
El diseño del lenguaje C # siempre se ha orientado (históricamente) a resolver problemas específicos en lugar de buscar resolver los problemas generales subyacentes.
Aunque hay algo de cierto en eso, no creo que sea una caracterización completamente justa o precisa, así que voy a comenzar mi respuesta negando la premisa de su pregunta.
Es cierto que existe un espectro con "muy específico" en un extremo y "muy general" en el otro, y que las soluciones a problemas específicos recaen en ese espectro. C # está diseñado como un todo para ser una solución muy general a muchos problemas específicos; eso es lo que es un lenguaje de programación de propósito general. Puede usar C # para escribir todo, desde servicios web hasta juegos de XBOX 360.
Como C # está diseñado para ser un lenguaje de programación de propósito general, cuando el equipo de diseño identifica un problema específico del usuario, siempre consideran el caso más general. LINQ es un excelente ejemplo de esto. En los primeros días del diseño de LINQ, era poco más que una forma de colocar sentencias SQL en un programa C #, porque ese es el espacio problema que se identificó. Pero muy pronto en el proceso de diseño, el equipo se dio cuenta de que los conceptos de clasificar, filtrar, agrupar y unir datos se aplicaban no solo a datos tabulares en una base de datos relacional, sino también a datos jerárquicos en XML y a objetos ad-hoc en la memoria. Y entonces decidieron buscar la solución mucho más general que tenemos hoy.
El truco del diseño es descubrir en qué parte del espectro tiene sentido detenerse. El equipo de diseño podría haber dicho, bueno, el problema de comprensión de las consultas es en realidad solo un caso específico del problema más general de las mónadas vinculantes. Y el problema de las mónadas vinculantes es en realidad solo un caso específico del problema más general de definir operaciones en tipos de tipos superiores. Y seguramente hay algo de abstracción sobre los sistemas de tipo ... y ya es suficiente. En el momento en que llegamos a la solución del problema bind-an-arbitrary-monad, la solución es ahora tan general que los programadores SQL de la línea de negocio que fueron la motivación para la función en primer lugar están completamente perdidos, y nos hemos en realidad resolvió su problema.
Las características realmente importantes agregadas desde C # 1.0 - tipos genéricos, funciones anónimas, bloques de iteradores, LINQ, dinámico, asíncrono - todas tienen la propiedad de que son características altamente generales útiles en muchos dominios diferentes. Todos pueden tratarse como ejemplos específicos de un problema más general, pero eso se aplica a cualquier solución a cualquier problema; siempre puedes hacerlo más general. La idea del diseño de cada una de estas características es encontrar el punto donde no puedan hacerse más generales sin confundir a sus usuarios .
Ahora que he negado la premisa de su pregunta, veamos la pregunta real:
Lo que trato de entender es a qué "construcción general" se refieren las palabras clave async / await
Depende de como lo veas.
La función async-await se basa en el tipo Task<T>
, que es como usted nota, una mónada. Y, por supuesto, si hablas de esto con Erik Meijer, inmediatamente señala que la Task<T>
es en realidad una comadrona ; puede obtener el valor de T
en el otro extremo.
Otra forma de ver la característica es tomar el párrafo que citó sobre los bloques del iterador y sustituir "iterador" por "asincrónico". Los métodos asíncronos son, como los métodos de iteración, un tipo de corrutina. Puede pensar en la Task<T>
como un simple detalle de implementación del mecanismo de corrutina si lo desea.
Una tercera forma de ver la característica es decir que es un tipo de llamada-con-continuación-actual (llamada / CC comúnmente abreviado). No es una implementación completa de call / cc porque no toma el estado de la pila de llamadas en el momento en que la continuación se registra en la cuenta. Vea esta pregunta para más detalles:
¿Cómo podría implementarse la nueva característica de asincronización en c # 5.0 con call / cc?
Voy a esperar y ver si alguien (Eric? Jon? ¿Quizás usted?) Puede completar más detalles sobre cómo en realidad C # genera código para implementar, aguarde,
La reescritura es esencialmente solo una variación de cómo se reescriben los bloques de iterador. Mads repasa todos los detalles en su artículo de MSDN Magazine: