Este artículo es bastante antiguo, pero no perdió su relevancia. Cuando se trata de asíncrono / espera, generalmente aparece un enlace. No pude encontrar una traducción al ruso, decidí ayudar a alguien que no habla con fluidez.
La programación asincrónica ha sido durante mucho tiempo el reino de los desarrolladores más experimentados con ansias de masoquismo: aquellos que tenían suficiente tiempo libre, inclinación y capacidad psíquica para pensar en las devoluciones de llamadas en un flujo de ejecución no lineal. Con el advenimiento de Microsoft .NET Framework 4.5, C # y Visual Basic nos trajeron a todos asincronía, por lo que los simples mortales ahora pueden escribir métodos asincrónicos casi tan fácilmente como los síncronos. Las devoluciones de llamada ya no son necesarias. No más código de cálculo explícito de un contexto de sincronización a otro. No más preocupaciones sobre cómo se mueven los resultados de ejecución o las excepciones. No hay necesidad de trucos que distorsionen los medios de los lenguajes de programación para la conveniencia de desarrollar código asincrónico. En resumen, no hay más problemas y dolor de cabeza.
Por supuesto, aunque ahora es fácil comenzar a escribir métodos asincrónicos (ver los artículos de Eric Lippert y Mads Torgersen en esta revista MSDN [OCTUBRE 2011] ), se requiere comprensión para hacerlo correctamente. lo que pasa debajo del capó Cada vez que un lenguaje o biblioteca eleva el nivel de abstracción que un desarrollador puede usar, esto inevitablemente se acompaña de costos ocultos que reducen la productividad. En muchos casos, estos costos son insignificantes, por lo que la mayoría de los programadores pueden descuidarlos en la mayoría de los casos. Sin embargo, los desarrolladores avanzados deben comprender completamente qué costos están presentes para tomar las medidas necesarias y resolver posibles problemas si se manifiestan. Esto es necesario cuando se utilizan herramientas de programación asincrónicas en C # y Visual Basic.
En este artículo, describiré las entradas y salidas de los métodos asincrónicos, describiré cómo se implementan los métodos asincrónicos y analizaré algunos de los costos más pequeños. Tenga en cuenta que esto no es una recomendación para distorsionar el código legible en algo que es difícil de mantener, en nombre de la microoptimización y el rendimiento. Esto es solo el conocimiento que ayudará a diagnosticar los problemas que pueda encontrar, y un conjunto de herramientas para superar estos problemas. Además, este artículo se basa en la vista previa de .NET Framework versión 4.5, y probablemente los detalles de implementación específicos pueden cambiar en la versión final.
Consigue un modelo de pensamiento cómodo
Durante décadas, los programadores han estado utilizando lenguajes de programación de alto nivel C #, Visual Basic, F # y C ++ para desarrollar aplicaciones productivas. Esta experiencia permitió a los programadores evaluar los costos de varias operaciones y obtener conocimiento sobre las mejores técnicas de desarrollo. Por ejemplo, en la mayoría de los casos, invocar un método síncrono es relativamente económico, especialmente si el compilador puede incrustar los contenidos del método invocado directamente en el punto de llamada. Por lo tanto, los desarrolladores están acostumbrados a dividir el código en métodos pequeños y fáciles de mantener, sin tener que preocuparse por las consecuencias negativas de aumentar el número de llamadas. El modelo de pensamiento de estos programadores está diseñado para manejar llamadas a métodos.
Con la llegada de los métodos asincrónicos, se requiere un nuevo modelo de pensamiento. C # y Visual Basic con sus compiladores pueden crear la ilusión de que el método asincrónico funciona como su contraparte síncrona, aunque todo está completamente mal por dentro. El compilador genera una gran cantidad de código para el programador, muy similar a la plantilla estándar que los desarrolladores escribieron para admitir la asincronía durante el tiempo en que era necesario hacerlo a mano. Además, el código que generó el compilador contiene llamadas a las funciones de la biblioteca de .NET Framework, lo que reduce aún más la cantidad de trabajo que un programador debe hacer. Para tener el modelo correcto de pensamiento y usarlo para tomar decisiones informadas, es importante comprender qué genera el compilador para usted.
Más métodos, menos llamadas.
Cuando se trabaja con código síncrono, la ejecución de métodos con contenido vacío es prácticamente inútil. Para métodos asincrónicos, este no es el caso. Considere este método asincrónico, que consiste en una instrucción (y que, debido a la falta de sentencias de espera, se ejecutará sincrónicamente):
public static async Task SimpleBodyAsync() { Console.WriteLine("Hello, Async World!"); }
Un descompilador de lenguaje intermedio (IL) revelará los verdaderos contenidos de esta función después de la compilación, dando como resultado algo similar a la Figura 1. Lo que era una línea simple convertida en dos métodos, uno de los cuales pertenece a la clase auxiliar de la máquina de estados. El primero es un método de código auxiliar que tiene una firma similar a la escrita por el programador (este método tiene el mismo nombre, el mismo alcance, toma los mismos parámetros y devuelve el mismo tipo), pero no contiene código escrito por el programador. Contiene solo una placa estándar para la configuración inicial. El código de configuración inicial inicializa la máquina de estados necesaria para representar el método asincrónico y lo inicia utilizando una llamada al método de utilidad MoveNext. El tipo de objeto de la máquina de estado contiene una variable con el estado de ejecución del método asincrónico, lo que le permite guardarlo al cambiar entre puntos de espera asincrónicos. También contiene código escrito por un programador, modificado para garantizar la transferencia de resultados de ejecución y excepciones al objeto de tarea devuelto; mantener la posición actual en el método para que la ejecución pueda continuar desde esta posición después de reanudar, etc.
Figura 1 Plantilla de método asincrónico
[DebuggerStepThrough] public static Task SimpleBodyAsync() { <SimpleBodyAsync>d__0 d__ = new <SimpleBodyAsync>d__0(); d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.MoveNext(); return d__.<>t__builder.Task; } [CompilerGenerated] [StructLayout(LayoutKind.Sequential)] private struct <SimpleBodyAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public void MoveNext() { try { if (this.<>1__state == -1) return; Console.WriteLine("Hello, Async World!"); } catch (Exception e) { this.<>1__state = -1; this.<>t__builder.SetException(e); return; } this.<>1__state = -1; this.<>t__builder.SetResult(); } ... }
Cuando se pregunte cuánto cuestan las llamadas a métodos asincrónicos, recuerde este patrón. El bloque try / catch en el método MoveNext es necesario para evitar un posible intento de incrustar este método por parte del compilador, por lo que al menos obtenemos el costo de la llamada al método, mientras que al usar el método sincrónico, lo más probable es que esta llamada no lo haga (siempre que contenido minimalista). Recibiremos varias llamadas a los procedimientos de Framework (por ejemplo, SetResult). Además de varias operaciones de escritura en los campos del objeto máquina de estado. Por supuesto, debemos comparar todos estos costos con los costos de Console.WriteLine, que probablemente prevalecerán (incluyen los costos de bloqueo, E / S, etc.) Preste atención a las optimizaciones que el entorno hace por usted. Por ejemplo, un objeto de una máquina de estados se implementa como una estructura (estructura). Esta estructura se encuadrará en un montón administrado solo si el método necesita pausar la ejecución, esperando que finalice la operación, y esto nunca sucederá en este método simple. Por lo tanto, el patrón de este método asincrónico no requerirá la asignación de memoria del montón. El compilador y el tiempo de ejecución intentarán minimizar el número de operaciones de asignación de memoria.
Cuándo no usar Async
.NET Framework intenta generar implementaciones eficientes para métodos asincrónicos utilizando varios métodos de optimización. Sin embargo, los desarrolladores, según su experiencia, a menudo aplican sus métodos de optimización, que pueden ser riesgosos y poco prácticos para la automatización por parte del compilador y el tiempo de ejecución, ya que intentan utilizar enfoques universales. Si no se olvida de esto, el rechazo del uso de métodos asíncronos es beneficioso en varios casos específicos, en particular, esto se aplica a los métodos en bibliotecas que se pueden usar con configuraciones más finas. Por lo general, esto sucede cuando se sabe con certeza que el método se puede ejecutar de forma sincrónica, ya que los datos de los que depende ya están listos.
Al crear métodos asincrónicos, los desarrolladores de .NET Framework dedicaron mucho tiempo a optimizar la cantidad de operaciones de administración de memoria. Esto es necesario porque la administración de memoria incurre en el mayor costo en el desempeño de una infraestructura asincrónica. La operación de asignación de memoria para un objeto suele ser relativamente económica. Asignar memoria para objetos es similar a llenar el carrito con productos en el supermercado: no gasta nada cuando los coloca en el carrito. El gasto ocurre cuando paga en la caja, saca su billetera y da dinero decente. Y si la asignación de memoria es fácil, la posterior recolección de basura puede afectar gravemente el rendimiento de la aplicación. Cuando comienza la recolección de basura, se realiza el escaneo y el marcado de los objetos que actualmente se encuentran en la memoria pero que no tienen enlaces. Cuantos más objetos se coloquen, más tiempo llevará marcarlos. Además, cuanto mayor sea el número de objetos de gran tamaño colocados, más a menudo se requiere la recolección de basura. Este aspecto de trabajar con la memoria tiene un impacto global en el sistema: cuanto más basura se produzca por métodos asincrónicos, más lenta será la ejecución de la aplicación, incluso si las microprotestas no demuestran costos significativos.
Para los métodos asincrónicos que suspenden su ejecución (esperando datos que aún no están listos), el entorno debe crear un objeto de tipo Tarea, que será devuelto por el método, ya que este objeto sirve como referencia única para la llamada. Sin embargo, a menudo se pueden hacer llamadas a métodos asincrónicos sin suspensión. Luego, el tiempo de ejecución puede devolver del caché el objeto de tarea completado previamente, que se usa una y otra vez sin la necesidad de crear nuevos objetos de tarea. Es cierto, esto solo se permite bajo ciertas condiciones, por ejemplo, cuando el método asincrónico devuelve un objeto no universal (no genérico) Tarea, Tarea, o cuando la Tarea universal se especifica mediante un tipo de referencia TResult, y el método devuelve un valor nulo. Aunque la lista de estas condiciones se expande con el tiempo, aún es mejor si sabe cómo se implementa la operación.
Considere una implementación de este tipo como MemoryStream. MemoryStream se hereda de Stream y redefine los nuevos métodos implementados en .NET 4.5: ReadAsync, WriteAsync y FlushAsync, para proporcionar una optimización de código específica de la memoria. Dado que la operación de lectura se realiza desde un búfer ubicado en la memoria, es decir, en realidad es una copia del área de memoria, el mejor rendimiento será si ReadAsync se ejecuta en modo síncrono. Una implementación de esto en un método asincrónico podría verse así:
public override async Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); return this.Read(buffer, offset, count); }
Bastante simple Y dado que Read es una llamada síncrona, y el método no tiene instrucciones de espera para controlar las expectativas, todas las llamadas a esta ReadAsync se ejecutarán de forma síncrona. Ahora veamos un caso estándar de uso de hilos, por ejemplo, una operación de copia:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Tenga en cuenta que en el ejemplo de ReadAsync dado, la secuencia fuente siempre se llama con el mismo parámetro de longitud de búfer, lo que significa que es muy probable que el valor de retorno (el número de bytes leídos) también se repita. Excepto en algunas circunstancias raras, es poco probable que la implementación de ReadAsync use el objeto Tarea en caché como valor de retorno, pero puede hacerlo.
Considere otra opción de implementación para este método, presentada en la Figura 2. Usando las ventajas de sus aspectos inherentes en escenarios estándar para este método, podemos optimizar la implementación al excluir las operaciones de asignación de memoria, lo cual es poco probable que se espere del tiempo de ejecución. Podemos eliminar por completo la pérdida de memoria devolviendo el mismo objeto Task que se usó en la llamada anterior ReadAsync si se leyó la misma cantidad de bytes. Y para una operación de tan bajo nivel, que probablemente será muy rápida y se llamará repetidamente, esta optimización tendrá un efecto significativo, especialmente en el número de recolecciones de basura.
Figura 2 Optimización de la creación de tareas.
private Task<int> m_lastTask; public override Task<int> ReadAsync(byte [] buffer, int offset, int count, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource<int>(); tcs.SetCanceled(); return tcs.Task; } try { int numRead = this.Read(buffer, offset, count); return m_lastTask != null && numRead == m_lastTask.Result ? m_lastTask : (m_lastTask = Task.FromResult(numRead)); } catch(Exception e) { var tcs = new TaskCompletionSource<int>(); tcs.SetException(e); return tcs.Task; } }
Se puede utilizar un método de optimización similar al eliminar la creación innecesaria de objetos de Tarea si es necesario el almacenamiento en caché. Considere un método diseñado para recuperar el contenido de una página web y almacenarlo en caché para referencia futura. Como método asincrónico, esto se puede escribir de la siguiente manera (usando la nueva biblioteca System.Net.Http.dll para .NET 4.5):
private static ConcurrentDictionary<string,string> s_urlToContents; public static async Task<string> GetContentsAsync(string url) { string contents; if (!s_urlToContents.TryGetValue(url, out contents)) { var response = await new HttpClient().GetAsync(url); contents = response.EnsureSuccessStatusCode().Content.ReadAsString(); s_urlToContents.TryAdd(url, contents); } return contents; }
Esta es una implementación de la frente. Y para las llamadas GetContentsAsync que no encuentran datos en la memoria caché, la sobrecarga de crear un nuevo objeto de Tarea puede descuidarse en comparación con el costo de recibir datos a través de la red. Sin embargo, en el caso de obtener datos de la memoria caché, estos costos se vuelven significativos si simplemente ajusta y proporciona los datos locales disponibles.
Para eliminar estos costos (si es necesario para lograr un alto rendimiento), puede volver a escribir el método como se muestra en la Figura 3. Ahora tenemos dos métodos: un método público síncrono y un método privado asíncrono, al que delega el público. La colección Diccionario ahora almacena en caché los objetos de Tarea creados, no sus contenidos, por lo que los intentos futuros de recuperar el contenido de una página que se obtuvo previamente con éxito se pueden realizar simplemente accediendo a la colección para devolver el objeto de Tarea existente. En el interior, puede aprovechar el uso de los métodos ContinueWith del objeto Task, que nos permite guardar el objeto ejecutado en la colección, en caso de que la carga de la página haya sido exitosa. Por supuesto, este código es más complejo y requiere mucho desarrollo y soporte, como es habitual cuando se optimiza el rendimiento: no querrá pasar tiempo escribiéndolo hasta que las pruebas de rendimiento muestren que estas complicaciones conducen a su mejora, lo cual es impresionante y obvio. Qué mejoras dependerán realmente del método de aplicación. Puede tomar un conjunto de pruebas que simule casos de uso comunes y evaluar los resultados para determinar si el juego vale la pena.
Figura 3 Tareas de almacenamiento en caché manual
private static ConcurrentDictionary<string,Task<string>> s_urlToContents; public static Task<string> GetContentsAsync(string url) { Task<string> contents; if (!s_urlToContents.TryGetValue(url, out contents)) { contents = GetContentsInternalAsync(url); contents.ContinueWith(delegate { s_urlToContents.TryAdd(url, contents); }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuatOptions.ExecuteSynchronously, TaskScheduler.Default); } return contents; } private static async Task<string> GetContentsInternalAsync(string url) { var response = await new HttpClient().GetAsync(url); return response.EnsureSuccessStatusCode().Content.ReadAsString(); }
Otro método de optimización asociado con los objetos de la Tarea es determinar si se devuelve dicho objeto del método asincrónico. Tanto C # como Visual Basic admiten métodos asincrónicos que devuelven un valor nulo (nulo), y no crean objetos de tarea en absoluto. Los métodos asincrónicos en las bibliotecas siempre deben devolver Tarea y Tarea, ya que al diseñar una biblioteca no se puede saber que no se usarán esperando su finalización. Sin embargo, al desarrollar aplicaciones, los métodos que devuelven vacío pueden encontrar su lugar. La razón principal de la existencia de tales métodos es proporcionar entornos existentes controlados por eventos, como ASP.NET y Windows Presentation Foundation (WPF). Utilizando async y wait, estos métodos facilitan la implementación de controladores de botones, eventos de carga de página, etc. Si tiene la intención de utilizar el método asincrónico con void, tenga cuidado al manejar las excepciones: las excepciones aparecerán en cualquier SynchronizationContext que estaba activo en el momento en que se llamó al método.
No olvides el contexto
Hay muchos contextos diferentes en .NET Framework: LogicalCallContext, SynchronizationContext, HostExecutionContext, SecurityContext, ExecutionContext y otros (su cantidad gigantesca puede sugerir que los creadores del Framework estaban económicamente motivados para crear nuevos contextos, pero estoy seguro de que esto no es así). Algunos de estos contextos afectan fuertemente los métodos asincrónicos, no solo en términos de funcionalidad, sino también en rendimiento.
SynchronizationContext SynchronizationContext juega un papel importante para los métodos asincrónicos. Un "contexto de sincronización" es solo una abstracción para garantizar que se invoque una invocación de delegado con los detalles de una biblioteca o entorno en particular. Por ejemplo, WPF tiene un DispatcherSynchronizationContext para representar una secuencia de interfaz de usuario (UI) para Dispatcher: enviar un delegado a este contexto de sincronización hace que este delegado se ponga en cola para su ejecución por el Dispatcher en su secuencia. ASP.NET proporciona un AspNetSynchronizationContext que se utiliza para garantizar que las operaciones asincrónicas involucradas en el procesamiento de una solicitud ASP.NET tengan garantizada su ejecución secuencial y estén vinculadas al estado HttpContext correcto. Bueno, etc. En general, hay alrededor de 10 especializaciones del SynchronizationContext en .NET Framework, algunas abiertas, otras internas.
Al esperar Tareas u objetos de otros tipos para los cuales .NET Framework puede implementar esto, los objetos que los esperan (por ejemplo, TaskAwaiter) capturan el SynchronizationContext actual en el momento en que comienza la espera (espera). Al finalizar la espera, si se capturó el SynchronizationContext, la continuación del método asincrónico se envía a este contexto de sincronización. Debido a esto, los programadores que escriben métodos asincrónicos que se invocan desde el flujo de la interfaz de usuario no necesitan reunir manualmente las llamadas al flujo de la interfaz de usuario para actualizar los controles de la interfaz de usuario: el Framework realiza este cálculo de referencias automáticamente.
Desafortunadamente, esta clasificación tiene un precio. Para los desarrolladores de aplicaciones que usan wait para implementar su flujo de control, la clasificación automática es la solución correcta. Las bibliotecas a menudo tienen una historia completamente diferente. Para los desarrolladores de aplicaciones, este cálculo de referencias es principalmente necesario para que el código controle el contexto en el que se ejecuta, por ejemplo, para acceder a los controles de la interfaz de usuario o para acceder al HttpContext correspondiente a la solicitud ASP.NET requerida. Sin embargo, generalmente no se requiere que las bibliotecas satisfagan tal requisito. Como resultado, el cálculo automático a menudo conlleva costos adicionales completamente innecesarios. Echemos otro vistazo al código que copia datos de una secuencia a otra:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0) { await source.WriteAsync(buffer, 0, numRead); }
Si se llama a esta copia desde la secuencia de IU, cada operación de lectura y escritura forzará la ejecución para volver a la secuencia de IU. En el caso de un megabyte de datos en la fuente y las secuencias que leen y escriben de forma asíncrona (es decir, la mayoría de sus implementaciones), esto significa unos 500 conmutadores de la secuencia de fondo a la secuencia de la interfaz de usuario. Para manejar este comportamiento en la tarea y los tipos de tarea, se crea el método ConfigureAwait. Este método acepta el parámetro continueOnCapturedContext de un tipo booleano que controla el cálculo de referencias. Si es verdadero (el valor predeterminado), esperar automáticamente devuelve el control al SynchronizationContext capturado. Si se utiliza falso, se ignorará el contexto de sincronización y el entorno continuará ejecutando la operación asincrónica en el subproceso donde se interrumpió. La implementación de esta lógica dará una versión más eficiente del código de copia entre hilos:
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
Para los desarrolladores de bibliotecas, tal aceleración en sí misma es suficiente para pensar siempre en usar ConfigureAwait, con la excepción de las raras condiciones en las que la biblioteca conoce lo suficiente sobre el tiempo de ejecución y tendrá que ejecutar el método con acceso al contexto correcto.
Además del rendimiento, hay otra razón por la que necesita usar ConfigureAwait al desarrollar bibliotecas. Imagine que el método CopyStreamToStreamAsync implementado con la versión del código sin ConfigureAwait se llama desde la secuencia de IU en WPF, por ejemplo, así:
private void button1_Click(object sender, EventArgs args) { Stream src = …, dst = …; Task t = CopyStreamToStreamAsync(src, dst); t.Wait(); // deadlock! }
En este caso, el programador tuvo que escribir button1_Click como un método asincrónico en el que se espera que el operador de espera ejecute la Tarea, y no use el método de espera sincrónico de este objeto. El método Wait debe usarse en muchos otros casos, pero casi siempre será un error usarlo para esperar en una secuencia de IU, como se muestra aquí. El método de espera no volverá hasta que se complete la tarea. En el caso de CopyStreamToStreamAsync, su secuencia asincrónica intenta devolver la ejecución con el envío de datos al SynchronizationContext capturado, y no puede completarse hasta que se completen dichas transferencias (porque son necesarias para continuar su operación). Pero estos despachos, a su vez, no se pueden ejecutar, porque la llamada de espera bloquea el subproceso de la interfaz de usuario que debe manejarlos. Esta es una dependencia cíclica que conduce a un punto muerto. Si CopyStreamToStreamAsync se implementa con ConfigureAwait (falso), no habrá dependencia ni bloqueo.
ExecutionContext ExecutionContext es una parte importante de .NET Framework, pero aún así la mayoría de los programadores desconocen su existencia. ExecutionContext – , SecurityContext LogicalCallContext, , . , ThreadPool.QueueUserWorkItem, Task.Run, Delegate.BeginInvoke, Stream.BeginRead, WebClient.DownloadStringAsync Framework, ExecutionContext ExecutionContext.Run ( ). , , ThreadPool.QueueUserWorkItem, Windows (identity), WaitCallback. , Task.Run LogicalCallContext, LogicalCallContext Action. ExecutionContext .
Framework , ExecutionContext, , . Windows LogicalCallContext . (WindowsIdentity.Impersonate CallContext.LogicalSetData) .
. C# Visual Basic , . await. , , - . C# Visual Basic («») , await (boxed) , .
. , . , , , .
C# Visual Basic , . ,
public static async Task FooAsync() { var dto = DateTimeOffset.Now; var dt = dto.DateTime; await Task.Yield(); Console.WriteLine(dt); }
dto await, . , , - dto:
Figure 4
[StructLayout(LayoutKind.Sequential), CompilerGenerated] private struct <FooAsync>d__0 : <>t__IStateMachine { private int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Action <>t__MoveNextDelegate; public DateTimeOffset <dto>5__1; public DateTime <dt>5__2; private object <>t__stack; private object <>t__awaiter; public void MoveNext(); [DebuggerHidden] public void <>t__SetMoveNextDelegate(Action param0); }
, . , , , , . , :
public static async Task FooAsync() { var dt = DateTimeOffset.Now.DateTime; await Task.Yield(); Console.WriteLine(dt); }
, .NET (GC) , , , : 0, , , (.NET GC 0, 1 2). , GC . , , , , , , . 0, , , . , , , .
( , ). JIT , , , , . , , . , , , , . , , . , C# Visual Basic , , .
C# Visual Basic , awaits: . await , Task , , . , , :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return Sum(await a, await b, await c); } private static int Sum(int a, int b, int c) { return a + b + c; }
C# “await b” Sum. await, Sum, - async , «» await. , await . , , CLR, , , . , <>t__stack. , , Tuple<int, int> <>__stack. , , , . , SumAsync :
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int ra = await a; int rb = await b; int rc = await c; return Sum(ra, rb, rc); }
, ra, rb rc, . , : . , , , . , , , , .
, , . Sum , await , . , await , . await , Task.WhenAll:
public static async Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { int [] results = await Task.WhenAll(a, b, c); return Sum(results[0], results[1], results[2]); }
Task.WhenAll Task<TResult[]>, , , , . . , WhenAll, Task Task. , , , , , WhenAll , . WhenAll, , , params, . , , . Figure 5
Figure 5
public static Task<int> SumAsync(Task<int> a, Task<int> b, Task<int> c) { return (a.Status == TaskStatus.RanToCompletion && b.Status == TaskStatus.RanToCompletion && c.Status == TaskStatus.RanToCompletion) ? Task.FromResult(Sum(a.Result, b.Result, c.Result)) : SumAsyncInternal(a, b, c); } private static async Task<int> SumAsyncInternal(Task<int> a, Task<int> b, Task<int> c) { await Task.WhenAll((Task)a, b, c).ConfigureAwait(false); return Sum(a.Result, b.Result, c.Result); }
, . , . , . , , : , , / , . .NET Framework , . , .NET Framework, . , , Framework, , , .