Come si crea un metodo asincrono in C #?


196

Ogni post sul blog che ho letto ti dice come consumare un metodo asincrono in C #, ma per qualche strana ragione non spiega mai come costruire i tuoi metodi asincroni da consumare. Quindi ho questo codice in questo momento che consuma il mio metodo:

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

E ho scritto questo metodo che è 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);
}

È questo, l'uso di Task.Factory, il modo migliore per scrivere un metodo asincrono o dovrei scriverlo in un altro modo?


22
Sto facendo una domanda generale su come strutturare un metodo. Voglio solo sapere da dove iniziare trasformando i miei metodi già sincroni in metodi asincroni.
Khalid Abuhakmeh,

2
OK, allora cosa fa un tipico metodo sincrono fare , e perché vuoi rendere asincrona ?
Eric Lippert,

Diciamo che devo elaborare in batch un mucchio di file e restituire un oggetto risultato.
Khalid Abuhakmeh,

1
OK, quindi: (1) qual è l'operazione ad alta latenza: ottenere i file - perché la rete potrebbe essere lenta, o qualsiasi altra cosa - o fare l'elaborazione - perché richiede molta CPU. E (2) non hai ancora detto perché vuoi che sia asincrono in primo luogo. Esiste un thread dell'interfaccia utente che non desideri bloccare o cosa?
Eric Lippert,

21
@EricLippert L'esempio fornito dall'op è molto semplice, in realtà non deve essere così complicato.
David B.

Risposte:


227

Non lo consiglio a StartNewmeno che non sia necessario quel livello di complessità.

Se il metodo asincrono dipende da altri metodi asincroni, l'approccio più semplice è utilizzare la asyncparola chiave:

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;
}

Se il tuo metodo asincrono sta facendo funzionare la CPU, dovresti usare Task.Run:

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

La mia async/ awaitintro potrebbe essere utile.


10
@Stephen: "Se il tuo metodo asincrono è dipendente da altri metodi asincroni" - ok, ma se non fosse così. E se cercassero di racimolare del codice che chiama BeginInvoke con un codice di callback?
Ricibob,

1
@Ricibob: dovresti usare TaskFactory.FromAsyncper avvolgere BeginInvoke. Non sei sicuro di cosa intendi per "codice di richiamata"; sentiti libero di pubblicare la tua domanda con il codice.
Stephen Cleary,

@Stephen: Grazie - sì TaskFactory.FromAsync è quello che stavo cercando.
Ricibob,

1
Stephen, nel ciclo for, è la prossima iterazione chiamata immediatamente o dopo i Task.Delayritorni?
jp2code,

3
@ jp2code: awaitè un "attesa asincrona", quindi non procede alla successiva iterazione fino a quando l'attività restituita viene Task.Delaycompletata.
Stephen Cleary,

12

Se non volevi usare async / await all'interno del tuo metodo, ma " decoralo " ancora per poter usare la parola chiave wait dall'esterno, 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; 
}

Da qui e qui

Per supportare un tale paradigma con i Task, abbiamo bisogno di un modo per mantenere la facciata del Task e la capacità di fare riferimento a un'operazione asincrona arbitraria come Task, ma per controllare la durata di quel Task secondo le regole dell'infrastruttura sottostante che fornisce il asincrono, e farlo in un modo che non costa in modo significativo. Questo è lo scopo di TaskCompletionSource.

Ho visto che viene utilizzato anche nel sorgente .NET ad es. 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;
    }

Infine, ho trovato utile anche quanto segue:

Mi viene sempre fatta questa domanda. L'implicazione è che ci deve essere qualche thread da qualche parte che sta bloccando la chiamata I / O alla risorsa esterna. Quindi, il codice asincrono libera il thread della richiesta, ma solo a spese di un altro thread altrove nel sistema, giusto? No, per niente. Per capire perché le richieste asincrone vengono ridimensionate, traccerò un esempio (semplificato) di una chiamata I / O asincrona. Supponiamo che una richiesta debba scrivere in un file. Il thread della richiesta chiama il metodo di scrittura asincrono. WriteAsync è implementato dalla Base Class Library (BCL) e utilizza le porte di completamento per il suo I / O asincrono. Pertanto, la chiamata WriteAsync viene passata al sistema operativo come scrittura asincrona dei file. Il sistema operativo comunica quindi con lo stack del driver, passando i dati per scrivere in un pacchetto di richieste I / O (IRP). Qui è dove le cose si fanno interessanti: Se un driver di dispositivo non è in grado di gestire immediatamente un IRP, deve gestirlo in modo asincrono. Quindi, il driver dice al disco di iniziare a scrivere e restituisce una risposta "in sospeso" al sistema operativo. Il sistema operativo passa tale risposta "in sospeso" al BCL e il BCL restituisce un'attività incompleta al codice di gestione delle richieste. Il codice di gestione delle richieste attende l'attività, che restituisce un'attività incompleta da quel metodo e così via. Infine, il codice di gestione delle richieste finisce per restituire un'attività incompleta ad ASP.NET e il thread della richiesta viene liberato per tornare al pool di thread. Il codice di gestione delle richieste attende l'attività, che restituisce un'attività incompleta da quel metodo e così via. Infine, il codice di gestione delle richieste finisce per restituire un'attività incompleta ad ASP.NET e il thread della richiesta viene liberato per tornare al pool di thread. Il codice di gestione delle richieste attende l'attività, che restituisce un'attività incompleta da quel metodo e così via. Infine, il codice di gestione delle richieste finisce per restituire un'attività incompleta ad ASP.NET e il thread della richiesta viene liberato per tornare al pool di thread.

Introduzione ad Async / Await su ASP.NET

Se l'obiettivo è migliorare la scalabilità (piuttosto che la reattività), tutto si basa sull'esistenza di un I / O esterno che offre l'opportunità di farlo.


1
Se vedrai il commento sopra l' implementazione Task.Run, ad esempio, accoda il lavoro specificato da eseguire sul ThreadPool e restituisce un handle di attività per quel lavoro . In realtà fai lo stesso. Sono sicuro che Task.Run sarà migliore, perché gestisce internamente CancelationToken e apporta alcune ottimizzazioni con la pianificazione e le opzioni di esecuzione.
unsafePtr

quindi quello che hai fatto con ThreadPool.QueueUserWorkItem potrebbe essere fatto con Task.Run, giusto?
Razvan,

-1

Un modo molto semplice per rendere asincrono un metodo è utilizzare il metodo Task.Yield (). Come afferma MSDN:

Puoi usare waitit Task.Yield (); in un metodo asincrono per forzare il completamento del metodo in modo asincrono.

Inseriscilo all'inizio del metodo e tornerà immediatamente al chiamante e completerà il resto del metodo su un altro thread.

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;
}

In un'applicazione WinForms il resto del metodo verrà completato nello stesso thread (il thread dell'interfaccia utente). La Task.Yield()è davvero utile, per i casi che si desidera assicurare che il restituito Tasknon sarà immediatamente completata al momento della creazione.
Theodor Zoulias,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.