Wie erstellen Sie eine asynchrone Methode in C #?

196

In jedem Blog-Beitrag, den ich gelesen habe, erfahren Sie, wie Sie eine asynchrone Methode in C # verwenden. Erklären Sie jedoch aus irgendeinem Grund niemals, wie Sie Ihre eigenen asynchronen Methoden zum Konsumieren erstellen. Ich habe jetzt diesen Code, der meine Methode verbraucht:

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

Und ich habe diese Methode geschrieben 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);
}

Ist dies Task.Factorydie beste Möglichkeit, eine asynchrone Methode zu schreiben, oder sollte ich dies anders schreiben?

Khalid Abuhakmeh
quelle
22
Ich stelle eine allgemeine Frage zur Strukturierung einer Methode. Ich möchte nur wissen, wo ich anfangen soll, meine bereits synchronen Methoden in asynchrone umzuwandeln.
Khalid Abuhakmeh
2
OK, so was sieht eine typische synchrone Methode tun , und warum wollen Sie es asynchron machen ?
Eric Lippert
Angenommen, ich muss eine Reihe von Dateien stapelweise verarbeiten und ein Ergebnisobjekt zurückgeben.
Khalid Abuhakmeh
1
OK, also: (1) Was ist die Operation mit hoher Latenz: Abrufen der Dateien - weil das Netzwerk langsam sein könnte oder was auch immer - oder Verarbeiten - weil es beispielsweise CPU-intensiv ist. Und (2) Sie haben immer noch nicht gesagt, warum Sie möchten, dass es überhaupt asynchron ist. Gibt es einen UI-Thread, den Sie nicht blockieren möchten, oder was?
Eric Lippert
21
@EricLippert Das Beispiel der Operation ist sehr einfach, es muss wirklich nicht so kompliziert sein.
David B.

Antworten:

226

Ich empfehle nicht, es StartNewsei denn, Sie benötigen diese Komplexität.

Wenn Ihre asynchrone Methode von anderen asynchronen Methoden abhängig ist, verwenden Sie am einfachsten das asyncSchlüsselwort:

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

Wenn Ihre asynchrone Methode CPU-Arbeit leistet, sollten Sie Folgendes verwenden Task.Run:

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

Sie können mein async/ awaitIntro hilfreich finden.

Stephen Cleary
quelle
10
@ Stephen: "Wenn Ihre asynchrone Methode von anderen asynchronen Methoden abhängt" - ok, aber was ist, wenn dies nicht der Fall ist? Was wäre, wenn Sie versuchen würden, einen Code, der BeginInvoke aufruft, mit einem Rückrufcode zu versehen?
Ricibob
1
@ Ricibob: Sie sollten TaskFactory.FromAsynczum Wickeln verwenden BeginInvoke. Nicht sicher, was Sie mit "Rückrufcode" meinen; Fühlen Sie sich frei, Ihre eigene Frage mit Code zu posten.
Stephen Cleary
@ Stephen: Danke - ja TaskFactory.FromAsync ist das, wonach ich gesucht habe.
Ricibob
1
Stephen, in der for-Schleife, wird die nächste Iteration unmittelbar oder nach der Task.DelayRückkehr aufgerufen ?
JP2-Code
3
@ jp2code: awaitist eine "asynchrone Wartezeit", daher wird erst mit der nächsten Iteration fortgefahren, nachdem die von zurückgegebene Aufgabe Task.Delayabgeschlossen ist.
Stephen Cleary
11

Wenn Sie async / await nicht in Ihrer Methode verwenden möchten, es aber dennoch "dekorieren" möchten, um das Schlüsselwort await von außen verwenden zu können, 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; 
}

Von hier und hier

Um ein solches Paradigma mit Aufgaben zu unterstützen, benötigen wir eine Möglichkeit, die Aufgabenfassade beizubehalten und eine beliebige asynchrone Operation als Aufgabe zu bezeichnen, aber die Lebensdauer dieser Aufgabe gemäß den Regeln der zugrunde liegenden Infrastruktur zu steuern, die die Aufgabe bereitstellt Asynchronität, und dies auf eine Weise, die nicht wesentlich kostet. Dies ist der Zweck von TaskCompletionSource.

Ich habe gesehen, dass es auch in der .NET-Quelle verwendet wird, z. 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;
    }

Schließlich fand ich auch folgendes nützlich:

Diese Frage wird mir die ganze Zeit gestellt. Die Implikation ist, dass irgendwo ein Thread vorhanden sein muss, der den E / A-Aufruf an die externe Ressource blockiert. Asynchroner Code gibt also den Anforderungsthread frei, aber nur auf Kosten eines anderen Threads an einer anderen Stelle im System, oder? Nein überhaupt nicht. Um zu verstehen, warum asynchrone Anforderungen skaliert werden, werde ich ein (vereinfachtes) Beispiel für einen asynchronen E / A-Aufruf verfolgen. Angenommen, eine Anforderung muss in eine Datei geschrieben werden. Der Anforderungsthread ruft die asynchrone Schreibmethode auf. WriteAsync wird von der Base Class Library (BCL) implementiert und verwendet Abschlussports für die asynchrone E / A. Daher wird der WriteAsync-Aufruf als asynchroner Dateischreibvorgang an das Betriebssystem weitergegeben. Das Betriebssystem kommuniziert dann mit dem Treiberstapel und leitet die Daten weiter, um sie in ein E / A-Anforderungspaket (IRP) zu schreiben. Hier wird es interessant: Wenn ein Gerätetreiber ein IRP nicht sofort verarbeiten kann, muss er es asynchron verarbeiten. Der Treiber weist die Festplatte an, mit dem Schreiben zu beginnen, und gibt eine ausstehende Antwort an das Betriebssystem zurück. Das Betriebssystem übergibt diese "ausstehende" Antwort an die BCL, und die BCL gibt eine unvollständige Aufgabe an den Anforderungsbearbeitungscode zurück. Der Anforderungsbearbeitungscode wartet auf die Aufgabe, die eine unvollständige Aufgabe von dieser Methode usw. zurückgibt. Schließlich gibt der Anforderungsbearbeitungscode eine unvollständige Aufgabe an ASP.NET zurück, und der Anforderungsthread wird freigegeben, um zum Threadpool zurückzukehren. Der Anforderungsbearbeitungscode wartet auf die Aufgabe, die eine unvollständige Aufgabe von dieser Methode usw. zurückgibt. Schließlich gibt der Anforderungsbearbeitungscode eine unvollständige Aufgabe an ASP.NET zurück, und der Anforderungsthread wird freigegeben, um zum Threadpool zurückzukehren. Der Anforderungsbearbeitungscode wartet auf die Aufgabe, die eine unvollständige Aufgabe von dieser Methode usw. zurückgibt. Schließlich gibt der Anforderungsbearbeitungscode eine unvollständige Aufgabe an ASP.NET zurück, und der Anforderungsthread wird freigegeben, um zum Threadpool zurückzukehren.

Einführung in Async / Await unter ASP.NET

Wenn das Ziel darin besteht, die Skalierbarkeit (und nicht die Reaktionsfähigkeit) zu verbessern, hängt alles von der Existenz einer externen E / A ab, die die Möglichkeit dazu bietet.

Alberto
quelle
1
Wenn Sie den Kommentar über der Task.Run-Implementierung sehen, z. B. Warteschlangen für die angegebene Arbeit, die auf dem ThreadPool ausgeführt werden soll, und Rückgabe eines Task-Handles für diese Arbeit . Du machst tatsächlich das Gleiche. Ich bin sicher, dass Task.Run besser sein wird, da es CancelationToken intern verwaltet und einige Optimierungen mit Planungs- und Ausführungsoptionen vornimmt.
unsafePtr
Was Sie mit ThreadPool.QueueUserWorkItem gemacht haben, konnte mit Task.Run gemacht werden, richtig?
Razvan
-1

Eine sehr einfache Möglichkeit, eine Methode asynchron zu machen, ist die Verwendung der Task.Yield () -Methode. Wie MSDN feststellt:

Sie können await Task.Yield () verwenden. in einer asynchronen Methode, um zu erzwingen, dass die Methode asynchron abgeschlossen wird.

Fügen Sie es am Anfang Ihrer Methode ein und es kehrt sofort zum Aufrufer zurück und schließt den Rest der Methode in einem anderen Thread ab.

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
quelle
In einer WinForms-Anwendung wird der Rest der Methode im selben Thread (dem UI-Thread) abgeschlossen. Dies Task.Yield()ist in der Tat nützlich, wenn Sie sicherstellen möchten, dass die Rücksendung Taskbei der Erstellung nicht sofort abgeschlossen wird.
Theodor Zoulias