c# - method - Async/await ID de hilo diferente
task void c# (4)
Estuve leyendo sobre async / await recientemente y estoy confundido con el hecho de que muchos de los artículos / publicaciones que estaba leyendo dicen que no se crea un nuevo hilo al usar async await ( Example ).
He creado una aplicación de consola simple para probarlo.
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait();
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync();
}
private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1);
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
}
La salida del siguiente código es:
Main: 8
Main Async: 8
thisIsAsyncStart: 8
thisIsAsyncEnd: 9
Main End: 8
¿Me estoy perdiendo el punto o esta ID de Issync está teniendo un ID de hilo diferente a otras acciones?
EDITAR:
He actualizado el código como se sugiere en la respuesta a continuación para await Task.Delay(1)
, pero todavía estoy viendo los mismos resultados.
Cita de la respuesta a continuación:
Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously
Quiero saber dónde se ejecuta la parte de asynchronously
, si no hay otros subprocesos creados? Si se ejecuta en el mismo subproceso, ¿no debería bloquearlo debido a una solicitud de E / S larga, o el compilador es lo suficientemente inteligente como para mover esa acción a otro subproceso si demora demasiado, y se usa un nuevo subproceso después de todo?
Hacer el método con async
no significa que creará otro hilo. Si RunTime ve que el método que se llama con await
en su método async
se retrasa, sale de ese método y espera después de que finalicen los métodos esperados y luego continúe ese método con otro hilo. Intente cambiar su Task.Delay(2000)
a Task.Delay(0)
y verá que no crea un nuevo hilo .
RunTime lo contará, si necesita crearlo, creará si no, no. Intenté su ejemplo con 0 ms y obtuve todo el mismo hilo:
Main: 1
Main Async: 1
thisIsAsyncStart: 1
thisIsAsyncEnd: 1
Main End: 1
Tomado del blog de Stephen Toub :
La palabra clave "asíncrono"
¿Qué hace la palabra clave "async" cuando se aplica a un método?
Cuando marca un método con la palabra clave "async", realmente le está diciendo al compilador dos cosas:
- Le está diciendo al compilador que desea poder usar la palabra clave "await" dentro del método (puede usar la palabra clave await si y solo si el método o lambda en el que está está marcado como asíncrono). Al hacerlo, le está diciendo al compilador que compile el método utilizando una máquina de estados, de modo que el método pueda suspenderse y luego reanudarse de forma asíncrona en los puntos de espera.
- Le está diciendo al compilador que "levante" el resultado del método o cualquier excepción que pueda ocurrir en el tipo de retorno. Para un método que devuelve Tarea o Tarea, esto significa que cualquier valor devuelto o excepción que no se maneje dentro del método se almacena en la tarea resultante. Para un método que devuelve void, esto significa que cualquier excepción se propaga al contexto de la persona que llama a través de cualquier "SynchronizationContext" que estaba vigente en el momento de la invocación inicial del método.
¿Usar la palabra clave "asíncrono" en un método hace que todas las invocaciones de ese método sean asíncronas?
No. Cuando invocas un método marcado como "asíncrono", comienza a ejecutarse sincrónicamente en el hilo actual. Por lo tanto, si tiene un método síncrono que devuelve vacío y todo lo que hace para cambiarlo es marcarlo como "asíncrono", las invocaciones de ese método aún se ejecutarán de forma sincrónica. Esto es cierto independientemente de si deja el tipo de retorno como "nulo" o lo cambia a "Tarea". De manera similar, si tiene un método síncrono que devuelve algún TResult, y todo lo que hace es marcarlo como "asíncrono" y cambiar el tipo de retorno a "Tarea", las invocaciones de ese método aún se ejecutarán de manera síncrona.
Marcar un método como "asíncrono" no afecta si el método se ejecuta de forma síncrona o asíncrona. Más bien, permite que el método se divida en varias partes, algunas de las cuales pueden ejecutarse de forma asíncrona, de manera que el método se puede completar de forma asíncrona. Los límites de estas piezas solo pueden ocurrir cuando usted codifica explícitamente una con la palabra clave "await", por lo que si "esperar" no se usa en absoluto en el código de un método, solo habrá una pieza, y como esa pieza comenzará a ejecutarse sincrónicamente, (y todo el método con él) se completará de forma sincrónica.
Me preguntaba exactamente lo mismo. Para mí las explicaciones de MSDN fueron contradictorias:
Las palabras clave async y espera no hacen que se creen subprocesos adicionales. Los métodos asíncronos no requieren subprocesos múltiples porque un método asíncrono no se ejecuta en su propio hilo .
MSDN: Programación asíncrona con async y espera
Una expresión de espera no bloquea el hilo en el que se está ejecutando. [..] Cuando la tarea se completa, invoca su continuación y la ejecución del método asíncrono se reanuda donde se detuvo.
No entendí cómo el hilo original no podía ser bloqueado, sin el uso de hilos adicionales. Además, el texto "invocar" sugirió que hay varios subprocesos utilizados en algún lugar y de alguna manera.
Pero luego me di cuenta, que todo está escrito correctamente, que estas palabras clave no utilizan ningún otro hilo. Es por diseño de la clase de Task
proporcionar mecanismos que pueden usar subprocesos diferentes, o no.
Mientras stephen-cleary explicaba bellamente estos mecanismos para el método Task.Delay()
, extendí el ejemplo de MSDN para saber cómo se comporta la await
con Task.Run()
:
private async void ds_StartButton_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine);
// Call the method that runs asynchronously.
string result = await WaitAsynchronouslyAsync();
// Call the method that runs synchronously.
//string result = await WaitSynchronously ();
// Do other Schdaff
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine);
// Display the result.
textBox1.Text += result;
}
// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()");
await Task.Delay(10000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ...");
await Task.Run(() => LongComputation());
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ...");
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine;
}
// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
// Add a using directive for System.Threading.
Thread.Sleep(10000);
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine;
}
private void ds_ButtonTest_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine);
Task<string> l_Task = WaitAsynchronouslyAsync();
//WaitAsynchronouslyAsync();
//textBox1.AppendText(l_Result);
}
private void LongComputation()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ...");
string l_RandomString = GetRandomString(10000000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated.");
}
/// <summary>Get random string with specified length</summary>
/// <param name="p_Length">Requested length of random string</param>
/// <param name="p_NoDots">Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore.
/// But due to huge performance implication, added this parameter to switch this off.</param>
/// <returns>Random string</returns>
public static string GetRandomString(int p_Length, bool p_NoDots = true)
{
StringBuilder l_StringBuilder = new StringBuilder();
string l_RandomString = string.Empty;
while (l_StringBuilder.Length <= p_Length)
{
l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName());
l_StringBuilder.Append(l_RandomString);
}
l_RandomString = l_StringBuilder.ToString(0, p_Length);
l_StringBuilder = null;
return l_RandomString;
}
Como se puede ver en la salida, se utilizan varios subprocesos, no por async/await
Task.Run()
, sino por Task.Run()
:
04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync()
04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ...
04.11.2016 12:38:17 [12] Generating random string ...
04.11.2016 12:38:21 [12] Random string generated.
04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ...
Esto es tan habitual como siempre lo ha sido, pero personalmente necesitaba este ejemplo explícito para comprender lo que está sucediendo y para separar lo que hace async/await
y lo que hace Task
.
Muy buena explicación para su pregunta aquí https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
Cuando se invoca el método principal de su aplicación de consola, SynchronizationContext.Current devolverá nulo. Eso significa que si invoca un método asíncrono en su aplicación de consola, a menos que haga algo especial, sus métodos asíncronos no tendrán afinidad de subprocesos: las continuaciones dentro de esos métodos asincrónicos podrían terminar ejecutándose "en cualquier lugar".
Te recomiendo que leas mi publicación de introducción async
para comprender la async
y await
palabras clave. En particular, await
(por defecto) capturará un "contexto" y usará ese contexto para reanudar su método asíncrono. Este "contexto" es el actual SynchronizationContext
(o TaskScheduler
, si no hay un SynchronzationContext
).
Quiero saber dónde se ejecuta la parte de forma asíncrona, si no hay otros subprocesos creados? Si se ejecuta en el mismo subproceso, ¿no debería bloquearlo debido a una solicitud de E / S larga, o el compilador es lo suficientemente inteligente como para mover esa acción a otro subproceso si demora demasiado, y se usa un nuevo subproceso después de todo?
Como explico en mi blog, las operaciones verdaderamente asíncronas no se "ejecutan" en ningún lugar . En este caso particular ( Task.Delay(1)
), la operación asíncrona se basa en un temporizador, no en un hilo bloqueado en algún lugar haciendo un Thread.Sleep
. La mayoría de I / O se hace de la misma manera. HttpClient.GetAsync
por ejemplo, se basa en E / S superpuestas (asíncronas), no en un hilo bloqueado en algún lugar que espera a que se complete la descarga de HTTP.
Una vez que entienda cómo await
utiliza su contexto, es más fácil recorrer el código original:
static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}
private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
- El hilo principal comienza a ejecutar
Main
y llama aMainAsync
. - El hilo principal está ejecutando
MainAsync
y llama athisIsAsync
. - El hilo principal está ejecutando
thisIsAsync
y llama aTask.Delay
. -
Task.Delay
hace lo que debe (iniciar un temporizador y otras cosas) y devuelve una tarea incompleta (tenga en cuenta queTask.Delay(0)
devolverá una tarea completada, lo que altera el comportamiento). - El hilo principal regresa a este
thisIsAsync
y espera la tarea devuelta desdeTask.Delay
. Dado que la tarea está incompleta, devuelve una tarea incompleta dethisIsAsync
. - El hilo principal regresa a
MainAsync
y aguarda la tarea devuelta porthisIsAsync
. Dado que la tarea está incompleta, devuelve una tarea incompleta deMainAsync
. - El hilo principal vuelve a
Main
y llama aWait
en la tarea devuelta desdeMainAsync
. Esto bloqueará el hilo principal hasta que se completeMainAsync
. - Cuando el temporizador establecido por
Task.Delay
se apague,thisIsAsync
continuará ejecutándose. Como no hay ningúnSynchronizationContext
oTaskScheduler
capturado por esaawait
, se reanuda la ejecución en un subproceso de grupo de subprocesos. - El subproceso del grupo de subprocesos llega al final de
thisIsAsync
, que completa su tarea. -
MainAsync
sigue ejecutándose. Como no hay ningún contexto capturado por esaawait
, se reanuda la ejecución en un subproceso de grupo de subprocesos (en realidad, el mismo subproceso quethisIsAsync
). - El subproceso del grupo de subprocesos llega al final de
MainAsync
, que completa su tarea. - El hilo principal regresa de su llamada a
Wait
y continúa ejecutando el métodoMain
. El subproceso del grupo de subprocesos utilizado para continuarthisIsAsync
yMainAsync
ya no es necesario y vuelve al grupo de subprocesos.
Lo importante aquí es que el grupo de hilos se utiliza porque no hay contexto . No se utiliza automáticamente "cuando es necesario". Si MainAsync
que ejecutar el mismo código MainAsync
/ thisIsAsync
dentro de una aplicación GUI, vería un uso de subprocesos muy diferente: los subprocesos de la interfaz de usuario tienen un SynchronizationContext
que programa las continuaciones en el subproceso de la interfaz de usuario, por lo que todos los métodos se reanudarán en ese mismo subproceso de la interfaz de usuario .