without whenall understanding method httpresponsemessage español ejemplo create await async c# .net asynchronous async-await

c# - whenall - ¿Cómo ceder y esperar implementar el flujo de control en.NET?



task whenall c# (5)

Según entiendo la palabra clave de yield , si se usa desde el interior de un bloque iterador, devuelve el flujo de control al código de llamada, y cuando se llama nuevamente al iterador, continúa donde se quedó.

Además, await no solo espera a la persona que llama, sino que le devuelve el control a la persona que llama, solo para retomar donde lo dejó cuando la persona que llama awaits el método.

En otras palabras, no hay ningún hilo , y la "concurrencia" de asíncrono y espera es una ilusión causada por un flujo inteligente de control, cuyos detalles están ocultos por la sintaxis.

Ahora, soy un antiguo programador de ensamblaje y estoy muy familiarizado con punteros de instrucción, pilas, etc. y entiendo cómo funcionan los flujos normales de control (subrutina, recursión, bucles, ramas). Pero estas nuevas construcciones ... no las entiendo.

Cuando se alcanza una await , ¿cómo sabe el tiempo de ejecución qué fragmento de código debe ejecutarse a continuación? ¿Cómo sabe cuándo puede reanudar donde lo dejó y cómo recuerda dónde? ¿Qué le sucede a la pila de llamadas actual? ¿Se guarda de alguna manera? ¿Qué sucede si el método de llamada realiza otras llamadas de método antes de await ¿Por qué no se sobrescribe la pila? ¿Y cómo diablos el tiempo de ejecución se abrirá paso a través de todo esto en el caso de una excepción y un apilamiento?

Cuando se alcanza el yield , ¿cómo hace el tiempo de ejecución para hacer un seguimiento del punto donde las cosas deben ser recogidas? ¿Cómo se conserva el estado del iterador?


Contestaré sus preguntas específicas a continuación, pero probablemente haría bien en simplemente leer mis extensos artículos sobre cómo diseñamos el rendimiento y la espera.

https://blogs.msdn.microsoft.com/ericlippert/tag/continuation-passing-style/

https://blogs.msdn.microsoft.com/ericlippert/tag/iterators/

https://blogs.msdn.microsoft.com/ericlippert/tag/async/

Algunos de estos artículos están desactualizados ahora; El código generado es diferente de muchas maneras. Pero esto sin duda le dará la idea de cómo funciona.

Además, si no comprende cómo se generan las lambdas como clases de cierre, comprenda eso primero . No harás caras o colas asincrónicas si no tienes lambdas caídas.

Cuando se alcanza una espera, ¿cómo sabe el tiempo de ejecución qué fragmento de código debe ejecutarse a continuación?

await se genera como:

if (the task is not completed) assign a delegate which executes the remainder of the method as the continuation of the task return to the caller else execute the remainder of the method now

Eso es básicamente todo. Esperar es solo un regreso elegante.

¿Cómo sabe cuándo puede reanudar donde lo dejó y cómo recuerda dónde?

Bueno, ¿cómo haces eso sin esperar? Cuando el método foo llama a la barra de métodos, de alguna manera recordamos cómo volver al medio de foo, con todos los locales de la activación de foo intactos, sin importar lo que haga la barra.

Ya sabes cómo se hace en ensamblador. Se empuja un registro de activación para foo a la pila; Contiene los valores de los locales. En el punto de la llamada, la dirección de retorno en foo se inserta en la pila. Cuando finaliza la barra, el puntero de la pila y el puntero de instrucción se restablecen a donde deben estar y foo continúa desde donde se quedó.

La continuación de una espera es exactamente la misma, excepto que el registro se coloca en el montón por la razón obvia de que la secuencia de activaciones no forma una pila .

El delegado que espera da como continuación de la tarea contiene (1) un número que es la entrada a una tabla de búsqueda que proporciona el puntero de instrucción que necesita ejecutar a continuación, y (2) todos los valores de locales y temporales.

Hay algo de equipo adicional allí; por ejemplo, en .NET es ilegal ramificarse en el medio de un bloque de prueba, por lo que no puede simplemente pegar la dirección del código dentro de un bloque de prueba en la tabla. Pero estos son detalles de contabilidad. Conceptualmente, el registro de activación simplemente se mueve al montón.

¿Qué le sucede a la pila de llamadas actual? ¿Se guarda de alguna manera?

La información relevante en el registro de activación actual nunca se coloca en la pila en primer lugar; se asigna fuera del montón desde el primer momento. (Bueno, los parámetros formales se pasan normalmente en la pila o en los registros y luego se copian en una ubicación de almacenamiento dinámico cuando comienza el método).

Los registros de activación de las personas que llaman no se almacenan; la espera probablemente va a volver a ellos, recuerde, por lo que se tratarán normalmente.

Tenga en cuenta que esta es una diferencia relevante entre el estilo de paso de continuación simplificado de wait y las estructuras de llamada real con continuación actual que puede ver en lenguajes como Scheme. En esos idiomas, call-cc captura toda la continuación, incluida la continuación de la call-cc .

¿Qué sucede si el método de llamada realiza otras llamadas de método antes de esperar? ¿Por qué no se sobrescribe la pila?

Esas llamadas al método regresan, por lo que sus registros de activación ya no están en la pila en el punto de espera.

¿Y cómo diablos el tiempo de ejecución se abrirá paso a través de todo esto en el caso de una excepción y un apilamiento?

En el caso de una excepción no detectada, la excepción se captura, se almacena dentro de la tarea y se vuelve a generar cuando se obtiene el resultado de la tarea.

¿Recuerdas toda esa contabilidad que mencioné antes? Conseguir una semántica de excepción correcta fue un gran dolor, déjame decirte.

Cuando se alcanza el rendimiento, ¿cómo hace el tiempo de ejecución para hacer un seguimiento del punto donde las cosas deben ser recogidas? ¿Cómo se conserva el estado del iterador?

Mismo camino. El estado de los locales se mueve al montón, y un número que representa la instrucción en la que MoveNext debe reanudarse la próxima vez que se llama se almacena junto con los locales.

Y de nuevo, hay un montón de cosas en un bloque iterador para asegurarse de que las excepciones se manejen correctamente.


Normalmente, recomendaría mirar el CIL, pero en el caso de estos, es un desastre.

Estas dos construcciones de lenguaje son similares en el trabajo, pero implementadas de manera un poco diferente. Básicamente, es solo un azúcar sintáctico para una magia compiladora, no hay nada loco / inseguro en el nivel de ensamblaje. Miremos brevemente.

yield es una declaración más antigua y simple, y es un azúcar sintáctico para una máquina de estado básica. Un método que devuelve IEnumerable<T> o IEnumerator<T> puede contener un yield , que luego transforma el método en una fábrica de máquinas de estado. Una cosa que debe notar es que no se ejecuta ningún código en el método en el momento en que lo llama, si hay un yield dentro. La razón es que el código que escribe se transloca al IEnumerator<T>.MoveNext , que comprueba el estado en el que se encuentra y ejecuta la parte correcta del código. yield return x; luego se convierte en algo similar a esto. this.Current = x; return true; this.Current = x; return true;

Si reflexiona un poco, puede inspeccionar fácilmente la máquina de estado construida y sus campos (al menos uno para el estado y los locales). Incluso puede restablecerlo si cambia los campos.

await requiere un poco de soporte de la biblioteca de tipos y funciona de manera algo diferente. Toma un argumento Task o Task<T> , luego resulta en su valor si la tarea se completa o registra una continuación a través de Task.GetAwaiter().OnCompleted . La implementación completa del sistema async / en await tardaría demasiado en explicarse, pero tampoco es tan mística. También crea una máquina de estado y la pasa a lo largo de la continuación a OnCompleted . Si la tarea se completa, utiliza su resultado en la continuación. La implementación del camarero decide cómo invocar la continuación. Por lo general, utiliza el contexto de sincronización del hilo de llamada.

Tanto el yield como la await tienen que dividir el método en función de su ocurrencia para formar una máquina de estado, con cada rama de la máquina representando cada parte del método.

No debe pensar en estos conceptos en los términos de "nivel inferior" como pilas, hilos, etc. Estas son abstracciones, y su funcionamiento interno no requiere ningún apoyo del CLR, es solo el compilador el que hace la magia. Esto es muy diferente de las rutinas de Lua, que tienen el soporte del tiempo de ejecución, o el longjmp de C, que es solo magia negra.


Ya hay un montón de excelentes respuestas aquí; Solo voy a compartir algunos puntos de vista que pueden ayudar a formar un modelo mental.

Primero, el compilador divide un método async en varias partes; Las expresiones de await son los puntos de fractura. (Esto es fácil de concebir para métodos simples; los métodos más complejos con bucles y manejo de excepciones también se rompen, con la adición de una máquina de estado más compleja).

Segundo, await se traduce en una secuencia bastante simple; Me gusta la descripción de Lucian , que en palabras es más o menos "si lo esperado ya está completo, obtenga el resultado y continúe ejecutando este método; de lo contrario, guarde el estado de este método y regrese". (Utilizo una terminología muy similar en mi introducción async ).

Cuando se alcanza una espera, ¿cómo sabe el tiempo de ejecución qué fragmento de código debe ejecutarse a continuación?

El resto del método existe como una devolución de llamada para esa espera (en el caso de tareas, estas devoluciones de llamada son continuaciones). Cuando lo esperado se completa, invoca sus devoluciones de llamada.

Tenga en cuenta que la pila de llamadas no se guarda y restaura; las devoluciones de llamada se invocan directamente. En el caso de E / S superpuestas, se invocan directamente desde el grupo de subprocesos.

Esas devoluciones de llamada pueden continuar ejecutando el método directamente, o pueden programarlo para que se ejecute en otro lugar (por ejemplo, si la await capturó un UI SynchronizationContext y la E / S se completó en el grupo de subprocesos).

¿Cómo sabe cuándo puede reanudar donde lo dejó y cómo recuerda dónde?

Todo son solo devoluciones de llamada. Cuando se completa una espera, invoca sus devoluciones de llamada, y cualquier método async que ya había await se reanuda. La devolución de llamada salta a la mitad de ese método y tiene sus variables locales en el alcance.

Las devoluciones de llamada no se ejecutan en un subproceso particular y no tienen restaurada su pila de llamadas.

¿Qué le sucede a la pila de llamadas actual? ¿Se guarda de alguna manera? ¿Qué sucede si el método de llamada realiza otras llamadas de método antes de esperar? ¿Por qué no se sobrescribe la pila? ¿Y cómo diablos el tiempo de ejecución se abrirá paso a través de todo esto en el caso de una excepción y un apilamiento?

La pila de llamadas no se guarda en primer lugar; No es necesario.

Con el código síncrono, puede terminar con una pila de llamadas que incluye a todas las personas que llaman, y el tiempo de ejecución sabe a dónde regresar usando eso.

Con el código asincrónico, puede terminar con un montón de punteros de devolución de llamada, enraizados en alguna operación de E / S que finaliza su tarea, que puede reanudar un método async que finaliza su tarea, que puede reanudar un método async que finaliza su tarea, etc. .

Entonces, con el código síncrono A llama a B llama a C , su pila de llamadas puede verse así:

A:B:C

mientras que el código asincrónico utiliza devoluciones de llamada (punteros):

A <- B <- C <- (I/O operation)

Cuando se alcanza el rendimiento, ¿cómo hace el tiempo de ejecución para hacer un seguimiento del punto donde las cosas deben ser recogidas? ¿Cómo se conserva el estado del iterador?

Actualmente, bastante ineficientemente. :)

Funciona como cualquier otra lambda: las vidas variables se extienden y las referencias se colocan en un objeto de estado que vive en la pila. El mejor recurso para todos los detalles de nivel profundo es la serie EduAsync de Jon Skeet .


yield es el más fácil de los dos, así que vamos a examinarlo.

Digamos que tenemos:

public IEnumerable<int> CountToTen() { for (int i = 1; i <= 10; ++i) { yield return i; } }

Esto se compila un poco como si hubiéramos escrito:

// Deliberately use name that isn''t valid C# to not clash with anything private class <CountToTen> : IEnumerator<int>, IEnumerable<int> { private int _i; private int _current; private int _state; private int _initialThreadId = CurrentManagedThreadId; public IEnumerator<CountToTen> GetEnumerator() { // Use self if never ran and same thread (so safe) // otherwise create a new object. if (_state != 0 || _initialThreadId != CurrentManagedThreadId) { return new <CountToTen>(); } _state = 1; return this; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Current => _current; object IEnumerator.Current => Current; public bool MoveNext() { switch(_state) { case 1: _i = 1; _current = i; _state = 2; return true; case 2: ++_i; if (_i <= 10) { _current = _i; return true; } break; } _state = -1; return false; } public void Dispose() { // if the yield-using method had a `using` it would // be translated into something happening here. } public void Reset() { throw new NotSupportedException(); } }

Por lo tanto, no es tan eficiente como una implementación escrita a mano de IEnumerable<int> e IEnumerator<int> (por ejemplo, es probable que no desperdiciemos tener un _state , _i y _current por _current en este caso) pero no está mal (el truco de volver a usar cuando es seguro hacerlo en lugar de crear un nuevo objeto es bueno) y extensible para lidiar con métodos muy complicados de uso de yield .

Y por supuesto desde

foreach(var a in b) { DoSomething(a); }

Es lo mismo que:

using(var en = b.GetEnumerator()) { while(en.MoveNext()) { var a = en.Current; DoSomething(a); } }

Luego, el MoveNext() generado se llama repetidamente.

El caso async es más o menos el mismo principio, pero con un poco de complejidad adicional. Para reutilizar un ejemplo de otro código de respuesta como:

private async Task LoopAsync() { int count = 0; while(count < 5) { await SomeNetworkCallAsync(); count++; } }

Produce código como:

private struct LoopAsyncStateMachine : IAsyncStateMachine { public int _state; public AsyncTaskMethodBuilder _builder; public TestAsync _this; public int _count; private TaskAwaiter _awaiter; void IAsyncStateMachine.MoveNext() { try { if (_state != 0) { _count = 0; goto afterSetup; } TaskAwaiter awaiter = _awaiter; _awaiter = default(TaskAwaiter); _state = -1; loopBack: awaiter.GetResult(); awaiter = default(TaskAwaiter); _count++; afterSetup: if (_count < 5) { awaiter = _this.SomeNetworkCallAsync().GetAwaiter(); if (!awaiter.IsCompleted) { _state = 0; _awaiter = awaiter; _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this); return; } goto loopBack; } _state = -2; _builder.SetResult(); } catch (Exception exception) { _state = -2; _builder.SetException(exception); return; } } [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0) { _builder.SetStateMachine(param0); } } public Task LoopAsync() { LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine(); stateMachine._this = this; AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create(); stateMachine._builder = builder; stateMachine._state = -1; builder.Start(ref stateMachine); return builder.Task; }

Es más complicado, pero un principio básico muy similar. La principal complicación adicional es que ahora se está utilizando GetAwaiter() . Si se awaiter.IsCompleted tiempo de awaiter.IsCompleted , se devuelve true porque la tarea en await ya se completó (por ejemplo, casos en los que podría regresar sincrónicamente), entonces el método sigue avanzando por los estados, pero de lo contrario se configura como una devolución de llamada al camarero.

Lo que suceda con eso depende del camarero, en términos de lo que desencadena la devolución de llamada (por ejemplo, finalización de E / S asíncrona, una tarea que se ejecuta en un subproceso que se completa) y qué requisitos hay para ordenar a un subproceso particular o ejecutar en un subproceso de conjunto de subprocesos , qué contexto de la llamada original puede o no ser necesario, etc. Sin importar lo que sea, algo en ese mesero llamará a MoveNext y continuará con el siguiente trabajo (hasta el siguiente en await ) o finalizará y regresará, en cuyo caso la Task que está implementando se completa.


yield y await son, mientras ambos tratan con el control de flujo, dos cosas completamente diferentes. Así que los abordaré por separado.

El objetivo del yield es facilitar la construcción de secuencias perezosas. Cuando escribe un bucle enumerador con una declaración de yield , el compilador genera una tonelada de código nuevo que no ve. Debajo del capó, en realidad genera una clase completamente nueva. La clase contiene miembros que rastrean el estado del bucle y una implementación de IEnumerable para que cada vez que llame a MoveNext avance una vez más por ese bucle. Entonces, cuando haces un bucle foreach como este:

foreach(var item in mything.items()) { dosomething(item); }

el código generado se parece a:

var i = mything.items(); while(i.MoveNext()) { dosomething(i.Current); }

Dentro de la implementación de mything.items () hay un montón de código de máquina de estado que hará un "paso" del bucle y luego regresará. Entonces, mientras lo escribe en la fuente como un bucle simple, debajo del capó no es un bucle simple. Así que el truco del compilador. Si quiere verse a sí mismo, saque ILDASM o ILSpy o herramientas similares y vea cómo se ve el IL generado. Debería ser instructivo.

async y await , por otro lado, hay otra caldera de peces. Aguardar es, en resumen, una primitiva de sincronización. Es una manera de decirle al sistema "No puedo continuar hasta que esto se haga". Pero, como notó, no siempre hay un hilo involucrado.

Lo que está involucrado es algo llamado contexto de sincronización. Siempre hay uno dando vueltas. El trabajo del contexto de sincronización es programar las tareas que se están esperando y sus continuaciones.

Cuando dices await thisThing() , suceden un par de cosas. En un método asíncrono, el compilador en realidad divide el método en fragmentos más pequeños, cada fragmento es una sección "antes de esperar" y una sección "después de esperar" (o continuación). Cuando se ejecuta la espera, la tarea que se está esperando y la siguiente continuación, en otras palabras, el resto de la función, se pasa al contexto de sincronización. El contexto se encarga de programar la tarea, y cuando termina, el contexto ejecuta la continuación, pasando el valor de retorno que desee.

El contexto de sincronización es libre de hacer lo que quiera siempre que programe cosas. Podría usar el grupo de subprocesos. Podría crear un hilo por tarea. Podría ejecutarlos sincrónicamente. Los diferentes entornos (ASP.NET frente a WPF) proporcionan diferentes implementaciones de contexto de sincronización que hacen diferentes cosas según lo que sea mejor para sus entornos.

(Bonificación: ¿alguna vez se preguntó qué hace .ConfigurateAwait(false) ? Le está diciendo al sistema que no use el contexto de sincronización actual (generalmente basado en su tipo de proyecto - WPF vs ASP.NET por ejemplo) y en su lugar use el predeterminado, que usa el grupo de hilos).

Entonces, de nuevo, es un montón de trucos de compilación. Si observa el código generado, es complicado, pero debería poder ver lo que está haciendo. Este tipo de transformaciones son difíciles, pero deterministas y matemáticas, por eso es genial que el compilador las esté haciendo por nosotros.

PD: Hay una excepción a la existencia de contextos de sincronización predeterminados: las aplicaciones de consola no tienen un contexto de sincronización predeterminado. Visite el blog de Stephen Toub para obtener más información. Es un gran lugar para buscar información sobre async y await en general.