understanding run method español ejemplos create await async c# multithreading asynchronous async-await task-parallel-library

c# - run - Async y Await: ¿cómo se mantiene el orden de ejecución?



task.run c# (2)

La llamada del método ''GetPrimesCountAsync'' se pondrá en cola y se ejecutará en un subproceso agrupado.

No. await no inicia ningún tipo de procesamiento en segundo plano. Espera a que se complete el procesamiento existente. GetPrimesCountAsync de GetPrimesCountAsync hacer eso (por ejemplo, utilizando Task.Run ). Está más claro de esta manera:

var myRunningTask = GetPrimesCountAsync(); await myRunningTask;

El ciclo solo continúa cuando la tarea esperada se ha completado. Nunca hay más de una tarea pendiente.

Entonces, ¿cómo garantiza el CLR que las solicitudes serán procesadas en el orden en que fueron hechas?

El CLR no está involucrado.

Dudo que el compilador simplemente transforme el código en la forma anterior, ya que esto desacoplaría el método ''GetPrimesCountAsync'' del bucle for.

La transformación que muestra es básicamente correcta, pero observe que la siguiente iteración de bucle no se inicia de inmediato, sino en la devolución de llamada . Eso es lo que serializa la ejecución.

De hecho, estoy leyendo algunos temas sobre la Biblioteca paralela de tareas y la programación asíncrona con asincrónica y estoy a la espera. El libro "C # 5.0 in a Nutshell" indica que cuando se espera una expresión con la palabra clave await, el compilador transforma el código en algo como esto:

var awaiter = expression.GetAwaiter(); awaiter.OnCompleted (() => { var result = awaiter.GetResult();

Supongamos que tenemos esta función asíncrona (también del libro referido):

async Task DisplayPrimeCounts() { for (int i = 0; i < 10; i++) Console.WriteLine (await GetPrimesCountAsync (i*1000000 + 2, 1000000) + " primes between " + (i*1000000) + " and " + ((i+1)*1000000-1)); Console.WriteLine ("Done!"); }

La llamada del método ''GetPrimesCountAsync'' se pondrá en cola y se ejecutará en un subproceso agrupado. En general, invocar múltiples hilos desde dentro de un bucle for tiene el potencial de introducir condiciones de carrera.

Entonces, ¿cómo garantiza el CLR que las solicitudes serán procesadas en el orden en que fueron hechas? Dudo que el compilador simplemente transforme el código en la forma anterior, ya que esto desacoplaría el método ''GetPrimesCountAsync'' del bucle for.


Solo por simplicidad, voy a reemplazar su ejemplo por uno que sea ligeramente más simple, pero que tenga todas las mismas propiedades significativas:

async Task DisplayPrimeCounts() { for (int i = 0; i < 10; i++) { var value = await SomeExpensiveComputation(i); Console.WriteLine(value); } Console.WriteLine("Done!"); }

El orden se mantiene debido a la definición de su código. Imaginemos caminar a través de él.

  1. Este método se llama primero
  2. La primera línea de código es el ciclo for, por lo que i se inicializa.
  3. La comprobación del ciclo pasa, entonces vamos al cuerpo del ciclo.
  4. Se llama SomeExpensiveComputation . Debería devolver una Task<T> muy rápido, pero el trabajo que haría continuaría en segundo plano.
  5. El resto del método se agrega como una continuación de la tarea devuelta; continuará ejecutándose cuando termine esa tarea.
  6. Una vez que la tarea devuelta por SomeExpensiveComputation finaliza, almacenamos el resultado en value .
  7. value se imprime en la consola.
  8. GOTO 3; tenga en cuenta que la operación costosa existente ya ha finalizado antes de que lleguemos al paso 4 por segunda vez y comencemos el siguiente.

En cuanto a cómo el compilador de C # logra realmente el paso 5, lo hace creando una máquina de estado. Básicamente, cada vez que hay una etiqueta de await que indica dónde se quedó, y al comienzo del método (o después de que se reanude después de cualquier continuación de incendios) comprueba el estado actual, y hace un goto hasta el punto donde lo dejó . También necesita elevar todas las variables locales a los campos de una nueva clase para que se mantenga el estado de esas variables locales.

Ahora bien, esta transformación no se realiza realmente en el código C #, sino en IL, pero esta es una especie de equivalente moral del código que mostré arriba en una máquina de estados. Tenga en cuenta que esto no es válido C # (no puede entrar en aa for un ciclo como este, pero esa restricción no se aplica al código IL que realmente se usa. También habrá diferencias entre esto y lo que C # realmente hace, pero debe darle una idea básica de lo que está sucediendo aquí:

internal class Foo { public int i; public long value; private int state = 0; private Task<int> task; int result0; public Task Bar() { var tcs = new TaskCompletionSource<object>(); Action continuation = null; continuation = () => { try { if (state == 1) { goto state1; } for (i = 0; i < 10; i++) { Task<int> task = SomeExpensiveComputation(i); var awaiter = task.GetAwaiter(); if (!awaiter.IsCompleted) { awaiter.OnCompleted(() => { result0 = awaiter.GetResult(); continuation(); }); state = 1; return; } else { result0 = awaiter.GetResult(); } state1: Console.WriteLine(value); } Console.WriteLine("Done!"); tcs.SetResult(true); } catch (Exception e) { tcs.SetException(e); } }; continuation(); } }

Tenga en cuenta que he ignorado la cancelación de tareas por el bien de este ejemplo, he ignorado todo el concepto de capturar el contexto de sincronización actual, hay un poco más pasando con el manejo de errores, etc. No considere esto como una implementación completa.