¿Cómo se crea un método asincrónico en C #?

196

Cada publicación de blog que he leído te dice cómo consumir un método asincrónico en C #, pero por alguna extraña razón nunca explicas cómo construir tus propios métodos asincrónicos para consumir. Entonces tengo este código en este momento que consume mi método:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

Y escribí este método que es CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

¿Es esta, el uso de Task.Factory, la mejor manera de escribir un método asincrónico, o debería escribir esto de otra manera?

Khalid Abuhakmeh
fuente
22
Estoy haciendo una pregunta general sobre cómo estructurar un método. Solo quiero saber por dónde empezar para convertir mis métodos ya sincrónicos en asincrónicos.
Khalid Abuhakmeh
2
OK, así que lo hace un método sincrónico típica hacer y qué es lo que quieres para que sea asíncrona ?
Eric Lippert
Digamos que tengo que procesar por lotes un montón de archivos y devolver un objeto de resultado.
Khalid Abuhakmeh
1
Bien, entonces: (1) cuál es la operación de alta latencia: obtener los archivos, porque la red podría ser lenta, o lo que sea, o hacer el procesamiento, porque requiere mucha CPU, por ejemplo. Y (2) todavía no has dicho por qué quieres que sea asíncrono en primer lugar. ¿Hay un subproceso de interfaz de usuario que no desea bloquear, o qué?
Eric Lippert
21
@EricLippert El ejemplo dado por el operador es muy básico, realmente no necesita ser tan complicado.
David B.

Respuestas:

226

No lo recomiendo a StartNewmenos que necesite ese nivel de complejidad.

Si su método asincrónico depende de otros métodos asincrónicos, el enfoque más sencillo es usar la asyncpalabra clave:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Si su método asíncrono está haciendo trabajo en la CPU, debe usar Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Puede encontrar mi async/ awaitintroducción útil.

Stephen Cleary
fuente
10
@Stephen: "Si su método asincrónico depende de otros métodos asincrónicos", está bien, pero ¿y si ese no es el caso? ¿Qué pasaría si tratara de ajustar algún código que llame a BeginInvoke con algún código de devolución de llamada?
Ricibob
1
@Ricibob: debe usar TaskFactory.FromAsyncpara envolver BeginInvoke. No estoy seguro de lo que quiere decir sobre "código de devolución de llamada"; no dude en publicar su propia pregunta con código.
Stephen Cleary
@Stephen: Gracias, sí TaskFactory.FromAsync es lo que estaba buscando.
Ricibob
1
Stephen, en el ciclo for, ¿se llama a la siguiente iteración inmediatamente o después de los Task.Delayretornos?
jp2code
3
@ jp2code: awaites una "espera asincrónica", por lo que no pasa a la siguiente iteración hasta que la tarea devuelta se Task.Delaycomplete.
Stephen Cleary
11

Si no desea usar async / await dentro de su método, pero aún así "decorarlo" para poder usar la palabra clave wait desde afuera, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

De aquí y de aquí

Para soportar tal paradigma con las Tareas, necesitamos una forma de retener la fachada de la Tarea y la capacidad de referirnos a una operación asincrónica arbitraria como Tarea, pero para controlar la vida útil de esa Tarea de acuerdo con las reglas de la infraestructura subyacente que proporciona el asincronía, y hacerlo de una manera que no cueste mucho. Este es el propósito de TaskCompletionSource.

Vi que también se usa en la fuente .NET, por ejemplo. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Finalmente, encontré útil también lo siguiente:

Me hacen esta pregunta todo el tiempo. La implicación es que debe haber algún hilo en algún lugar que esté bloqueando la llamada de E / S al recurso externo. Entonces, el código asincrónico libera el hilo de solicitud, pero solo a expensas de otro hilo en otra parte del sistema, ¿verdad? No, en absoluto. Para comprender por qué escalan las solicitudes asíncronas, rastrearé un ejemplo (simplificado) de una llamada de E / S asíncrona. Digamos que una solicitud necesita escribir en un archivo. El subproceso de solicitud llama al método de escritura asincrónico. WriteAsync es implementado por la Base Class Library (BCL), y utiliza puertos de finalización para su E / S asíncrona. Entonces, la llamada WriteAsync se pasa al sistema operativo como una escritura de archivo asíncrono. Luego, el sistema operativo se comunica con la pila de controladores, pasando los datos para escribir en un paquete de solicitud de E / S (IRP). Aquí es donde las cosas se ponen interesantes: Si un controlador de dispositivo no puede manejar un IRP de inmediato, debe manejarlo de forma asincrónica. Entonces, el controlador le dice al disco que comience a escribir y devuelve una respuesta "pendiente" al sistema operativo. El sistema operativo pasa esa respuesta "pendiente" al BCL, y el BCL devuelve una tarea incompleta al código de manejo de solicitudes. El código de manejo de solicitudes espera la tarea, que devuelve una tarea incompleta de ese método, etc. Finalmente, el código de manejo de solicitudes termina devolviendo una tarea incompleta a ASP.NET, y el subproceso de solicitud se libera para volver al grupo de subprocesos. El código de manejo de solicitudes espera la tarea, que devuelve una tarea incompleta de ese método, etc. Finalmente, el código de manejo de solicitudes termina devolviendo una tarea incompleta a ASP.NET, y el subproceso de solicitud se libera para volver al grupo de subprocesos. El código de manejo de solicitudes espera la tarea, que devuelve una tarea incompleta de ese método, etc. Finalmente, el código de manejo de solicitudes termina devolviendo una tarea incompleta a ASP.NET, y el subproceso de solicitud se libera para volver al grupo de subprocesos.

Introducción a Async / Await en ASP.NET

Si el objetivo es mejorar la escalabilidad (en lugar de la capacidad de respuesta), todo depende de la existencia de una E / S externa que brinde la oportunidad de hacerlo.

Alberto
fuente
1
Si verá el comentario sobre la implementación de Task.Run, por ejemplo, pone en cola el trabajo especificado para que se ejecute en ThreadPool y devuelve un identificador de tarea para ese trabajo . Realmente haces lo mismo. Estoy seguro de que Task.Run será mejor, porque administra internamente CancelationToken y hace algunas optimizaciones con las opciones de programación y ejecución.
inseguroPtr
entonces, lo que hiciste con ThreadPool.QueueUserWorkItem podría hacerse con Task.Run, ¿verdad?
Razvan
-1

Una forma muy simple de hacer que un método sea asincrónico es usar el método Task.Yield (). Como dice MSDN:

Puede usar await Task.Yield (); en un método asincrónico para forzar que el método se complete de forma asincrónica.

Insértelo al comienzo de su método y luego regresará inmediatamente a la persona que llama y completará el resto del método en otro hilo.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
SalgoMato
fuente
En una aplicación WinForms, el resto del método se completará en el mismo hilo (el hilo de la interfaz de usuario). De Task.Yield()hecho, es útil, para los casos en que desea asegurarse de que la devolución Taskno se completará inmediatamente después de la creación.
Theodor Zoulias