Async ausführlichAsync in depth

Schreiben von E/A- und CPU-gebundenem asynchronen Code ist mit dem .NET Task-basierten asynchronen Modell einfach.Writing I/O- and CPU-bound asynchronous code is straightforward using the .NET Task-based async model. Das Modell wird durch die Typen Task und Task<T> und die C#- und Visual Basic-Schlüsselwörter async und await verfügbar gemacht.The model is exposed by the Task and Task<T> types and the async and await keywords in C# and Visual Basic. (Sprachspezifische Ressourcen finden Sie im Abschnitt Siehe auch.) Dieser Artikel erläutert den Einsatz von .NET-Async und bietet einen Einblick in das im Hintergrund verwendete Async-Framework.(Language-specific resources are found in the See also section.) This article explains how to use .NET async and provides insight into the async framework used under the covers.

Task und Task<T>Task and Task<T>

Tasks sind Konstrukte zum Implementieren dessen, was als Promise-Modell der Parallelität bezeichnet wird.Tasks are constructs used to implement what is known as the Promise Model of Concurrency. Kurz gesagt: Sie bieten Ihnen eine „Zusage“, dass die Arbeit zu einem späteren Zeitpunkt abgeschlossen wird, sodass Sie die Zusage mit einer sauberen API koordinieren können.In short, they offer you a "promise" that work will be completed at a later point, letting you coordinate with the promise with a clean API.

  • Task stellt einen einzelnen Vorgang dar, der keinen Wert zurückgibt.Task represents a single operation which does not return a value.
  • Task<T> stellt einen einzelnen Vorgang dar, der einen Wert des Typs T zurückgibt.Task<T> represents a single operation which returns a value of type T.

Es ist wichtig, Tasks als asynchron stattfindende Abstraktionen von Arbeit zu betrachten, und nicht als Abstraktion des Threadings.It’s important to reason about tasks as abstractions of work happening asynchronously, and not an abstraction over threading. Tasks werden standardmäßig auf dem aktuellen Thread ausgeführt und delegieren Arbeit nach Bedarf an das Betriebssystem.By default, tasks execute on the current thread and delegate work to the Operating System, as appropriate. Optional können Tasks explizit zur Ausführung auf einem separaten Thread über die Task.Run-API angefordert werden.Optionally, tasks can be explicitly requested to run on a separate thread via the Task.Run API.

Tasks machen ein API-Protokoll zum Überwachen des Ergebniswerts (im Fall von Task<T>) eines Tasks sowie zum Warten und Zugriff darauf verfügbar.Tasks expose an API protocol for monitoring, waiting upon and accessing the result value (in the case of Task<T>) of a task. Sprachintegration mit dem Schlüsselwort await bietet eine Abstraktion auf höherer Ebene für die Verwendung von Tasks.Language integration, with the await keyword, provides a higher-level abstraction for using tasks.

Mithilfe von await kann Ihre Anwendung bzw. Ihr Dienst sinnvolle Aufgaben erledigen, während ein Task ausgeführt wird, indem die Steuerung an seinen Aufrufer übergeben wird, bis der Task abgeschlossen ist.Using await allows your application or service to perform useful work while a task is running by yielding control to its caller until the task is done. Ihr Code muss sich nicht auf Rückrufe oder Ereignisse verlassen, um die Ausführung nach Abschluss des Tasks fortzusetzen.Your code does not need to rely on callbacks or events to continue execution after the task has been completed. Die Sprach- und Task-API-Integration erledigt dies für Sie.The language and task API integration does that for you. Bei Verwendung von Task<T> „enthüllt“ das Schlüsselwort await darüber hinaus den Wert, der bei Abschluss des Tasks zurückgegeben wird.If you’re using Task<T>, the await keyword will additionally "unwrap" the value returned when the Task is complete. Wie dies funktioniert, wird weiter unten erläutert.The details of how this works are explained further below.

Weitere Informationen zu Tasks und den verschiedenen Arten, mit ihnen zu interagieren, finden Sie in dem Artikel Task-based Asynchronous Pattern (Taskbasiertes asynchrones Muster (TAP)).You can learn more about tasks and the different ways to interact with them in the Task-based Asynchronous Pattern (TAP) topic.

Tieferer Einblick in Tasks für einen E/A-gebundenen VorgangDeeper Dive into Tasks for an I/O-Bound Operation

Im folgenden Abschnitt wird allgemein beschrieben, was mit einem normalen asynchronen E/A-Aufruf geschieht.The following section describes a 10,000 foot view of what happens with a typical async I/O call. Beginnen wir mit ein paar Beispielen.Let's start with a couple examples.

Im ersten Beispiel wird eine asynchrone Methode aufgerufen und ein aktiver Task zurückgegeben, der wahrscheinlich noch abgeschlossen werden muss.The first example calls an async method and returns an active task, likely yet to complete.

public Task<string> GetHtmlAsync()
{
    // Execution is synchronous here
    var client = new HttpClient();

    return client.GetStringAsync("http://www.dotnetfoundation.org");
}

Im zweiten Beispiel werden zusätzlich die Schlüsselwörter async und await auf den Task angewandt.The second example adds the use of the async and await keywords to operate on the task.

public async Task<string> GetFirstCharactersCountAsync(string url, int count)
{
    // Execution is synchronous here
    var client = new HttpClient();

    // Execution of GetFirstCharactersCountAsync() is yielded to the caller here
    // GetStringAsync returns a Task<string>, which is *awaited*
    var page = await client.GetStringAsync("http://www.dotnetfoundation.org");

    // Execution resumes when the client.GetStringAsync task completes,
    // becoming synchronous again.

    if (count > page.Length)
    {
        return page;
    }
    else
    {
        return page.Substring(0, count);
    }
}

Der Aufruf von GetStringAsync() erfolgt über .NET-Bibliotheken auf niedrigerer Ebene (möglicherweise mit Aufruf anderer Async-Methoden), bis er einen PInvoke-Interop-Aufruf in eine native Netzwerkbibliothek erreicht.The call to GetStringAsync() calls through lower-level .NET libraries (perhaps calling other async methods) until it reaches a P/Invoke interop call into a native networking library. Die native Bibliothek kann anschließend einen Aufruf in einen System-API-Aufruf durchführen (z.B. write() an einen Socket unter Linux).The native library may subsequently call into a System API call (such as write() to a socket on Linux). Ein Taskobjekt wird an der nativen/verwalteten Grenze erstellt, möglicherweise mit TaskCompletionSource.A task object will be created at the native/managed boundary, possibly using TaskCompletionSource. Das Taskobjekt wird durch die Ebenen nach oben weitergegeben, möglicherweise bearbeitet oder direkt zurückgegeben, schließlich an den ursprünglichen Aufrufer zurückgegeben.The task object will be passed up through the layers, possibly operated on or directly returned, eventually returned to the initial caller.

Im zweiten Beispiel oben wird ein Task<T>-Objekt von GetStringAsync zurückgegeben.In the second example above, a Task<T> object will be returned from GetStringAsync. Die Verwendung des Schlüsselworts await bewirkt, dass die Methode ein neu erstelltes Taskobjekt zurückgibt.The use of the await keyword causes the method to return a newly created task object. Die Steuerung wird von dieser Position in der GetFirstCharactersCountAsync-Methode an den Aufrufer zurückgegeben.Control returns to the caller from this location in the GetFirstCharactersCountAsync method. Die Methoden und Eigenschaften des Task<T>-Objekts ermöglichen Aufrufern, den Status des Tasks zu überwachen, der abgeschlossen wird, wenn der verbleibende Code in GetFirstCharactersCountAsync ausgeführt wurde.The methods and properties of the Task<T> object enable callers to monitor the progress of the task, which will complete when the remaining code in GetFirstCharactersCountAsync has executed.

Nach dem System-API-Aufruf befindet sich die Anforderung jetzt im Kernelraum und ist auf dem Weg in das Netzwerksubsystem des Betriebssystems (z.B. /net im Linux-Kernel).After the System API call, the request is now in kernel space, making its way to the networking subsystem of the OS (such as /net in the Linux Kernel). Hier behandelt das Betriebssystem die Netzwerkanforderung asynchron.Here the OS will handle the networking request asynchronously. Details können je nach verwendetem Betriebssystem variieren (der Gerätetreiberaufruf kann als Signal geplant werden, das an die Runtime zurückgesendet wird, oder ein Gerätetreiberaufruf kann durchgeführt und dann ein Signal zurückgesendet werden), aber letztendlich wird die Runtime darüber informiert, dass die Netzwerkanforderung ausgeführt wird.Details may be different depending on the OS used (the device driver call may be scheduled as a signal sent back to the runtime, or a device driver call may be made and then a signal sent back), but eventually the runtime will be informed that the networking request is in progress. Zu diesem Zeitpunkt wird die Arbeit für den Gerätetreiber entweder geplant, durchgeführt, oder sie ist bereits abgeschlossen (die Anforderung ist bereits erfolgt) – aber da all dies asynchron geschieht, kann der Gerätetreiber sofort etwas anderes verarbeiten!At this time, the work for the device driver will either be scheduled, in-progress, or already finished (the request is already out "over the wire") - but because this is all happening asynchronously, the device driver is able to immediately handle something else!

Beispielsweise richtet in Windows ein Betriebssystemthread einen Aufruf an den Netzwerkgerätetreiber und fordert ihn auf, den Netzwerkbetrieb über ein Interrupt Request Paket (IRP) durchzuführen, das den Vorgang darstellt.For example, in Windows an OS thread makes a call to the network device driver and asks it to perform the networking operation via an Interrupt Request Packet (IRP) which represents the operation. Der Gerätetreiber empfängt das IRP, führt den Aufruf an das Netzwerk durch, kennzeichnet das IRP als „ausstehend“ und kehrt zum Betriebssystem zurück.The device driver receives the IRP, makes the call to the network, marks the IRP as "pending", and returns back to the OS. Da der Betriebssystemthread nun weiß, dass das IRP „ausstehend“ ist, muss er keine weitere Arbeit für diesen Auftrag erledigen und „kehrt zurück“, sodass er zur Durchführung anderer Aufgaben verwendet werden kann.Because the OS thread now knows that the IRP is "pending", it doesn't have any more work to do for this job and "returns" back so that it can be used to perform other work.

Wenn die Anforderung erfüllt ist und die Daten über den Gerätetreiber zurückkommen, unterrichtet er die CPU über die neuen, über einen Interrupt empfangenen Daten.When the request is fulfilled and data comes back through the device driver, it notifies the CPU of new data received via an interrupt. Die Behandlung dieses Interrupts variiert je nach Betriebssystem, aber die Daten werden schließlich über das Betriebssystem übergeben, bis sie einen System-Interopaufruf erreichen (in Linux plant z.B. ein Interrupthandler die untere Hälfte der IRQ zum asynchronen Übergeben der Daten über das Betriebssystem ein).How this interrupt gets handled will vary depending on the OS, but eventually the data will be passed through the OS until it reaches a system interop call (for example, in Linux an interrupt handler will schedule the bottom half of the IRQ to pass the data up through the OS asynchronously). Beachten Sie, dass dies auch asynchron erfolgt!Note that this also happens asynchronously! Das Ergebnis wird in die Warteschlange gestellt, bis der nächste verfügbare Thread die Async-Methode ausführen und das Ergebnis des abgeschlossenen Tasks „enthüllen“ kann.The result is queued up until the next available thread is able execute the async method and "unwrap" the result of the completed task.

Ein wesentlicher Punkt bei diesem gesamten Prozess ist, dass kein Thread für die Ausführung des Tasks dediziert ist.Throughout this entire process, a key takeaway is that no thread is dedicated to running the task. Obwohl Arbeit in einem gewissen Kontext ausgeführt wird (d.h. das Betriebssystem muss Daten an einen Gerätetreiber übergeben und auf einen Interrupt reagieren), ist kein Thread für das Warten auf Daten dediziert, die von der Anforderung zurückgegeben werden.Although work is executed in some context (that is, the OS does have to pass data to a device driver and respond to an interrupt), there is no thread dedicated to waiting for data from the request to come back. Dadurch kann das System ein viel größeres Arbeitsvolumen bewältigen, anstatt auf den Abschluss eines E/A-Aufrufs zu warten.This allows the system to handle a much larger volume of work rather than waiting for some I/O call to finish.

Obwohl das Obige den Eindruck weckt, es sei viel Arbeit zu bewältigen, ist es in der Gesamtbetrachtungszeit winzig verglichen mit der zur Durchführung der eigentlichen E/A-Arbeit benötigten Zeit.Although the above may seem like a lot of work to be done, when measured in terms of wall clock time, it’s miniscule compared to the time it takes to do the actual I/O work. Eine potenzielle, wenn auch nicht präzise Zeitachse für solch einen Aufruf sieht wie folgt aus:Although not at all precise, a potential timeline for such a call would look like this:

0-1————————————————————————————————————————————————–2-30-1————————————————————————————————————————————————–2-3

  • Die zwischen den Punkten 0 und 1 verstrichene Zeit umfasst alles, bis eine Async-Methode die Steuerung an ihren Aufrufer übergibt.Time spent from points 0 to 1 is everything up until an async method yields control to its caller.
  • Die zwischen den Punkten 1 und 2 verstrichene Zeit ist die für E/A aufgewendete Zeit ohne CPU-Kosten.Time spent from points 1 to 2 is the time spent on I/O, with no CPU cost.
  • Schließlich wird die zwischen den Punkten 2 und 3 verstrichene Zeit für die Rückgabe der Steuerung (und möglicherweise eines Werts) an die Async-Methode aufgewendet. An diesem Punkt übernimmt sie wieder die Ausführung.Finally, time spent from points 2 to 3 is passing control back (and potentially a value) to the async method, at which point it is executing again.

Was bedeutet dies für ein Serverszenario?What does this mean for a server scenario?

Dieses Modell funktioniert gut mit einer normalen Serverszenario-Arbeitsauslastung.This model works well with a typical server scenario workload. Da keine Threads für das Blockieren unerledigter Tasks dediziert sind, kann der Serverthreadpool eine viel höhere Anzahl von Webanforderungen bedienen.Because there are no threads dedicated to blocking on unfinished tasks, the server threadpool can service a much higher volume of web requests.

Stellen Sie sich zwei Server vor: Auf dem einen wird Async-Code ausgeführt, auf dem anderen nicht.Consider two servers: one that runs async code, and one that does not. Im Rahmen dieses Beispiels stehen jedem Server nur 5 Threads für Dienstanforderungen zur Verfügung.For the purpose of this example, each server only has 5 threads available to service requests. Beachten Sie, dass diese Zahlen imaginär klein und nur in einem Demokontext sinnvoll sind.Note that these numbers are imaginarily small and serve only in a demonstrative context.

Stellen Sie sich vor, dass beide Server 6 gleichzeitige Anforderungen empfangen.Assume both servers receive 6 concurrent requests. Jede Anforderung führt einen E/A-Vorgang durch.Each request performs an I/O operation. Der Server ohne Async-Code muss die sechste Anforderung in die Warteschlange stellen, bis einer der 5 Threads die E/A-gebundene Arbeit abgeschlossen und eine Antwort geschrieben hat.The server without async code has to queue up the 6th request until one of the 5 threads have finished the I/O-bound work and written a response. Wenn die 20. Anforderung eingeht, könnte der Server beginnen, langsamer zu werden, da die Warteschlange zu lang wird.At the point that the 20th request comes in, the server might start to slow down, because the queue is getting too long.

Der Server mit Async-Code stellt die sechste Anforderung auch in die Warteschlange, aber da er async und await verwendet, wird jeder seiner Threads freigegeben, wenn die E/A-gebundene Arbeit startet, nicht bei deren Abschluss.The server with async code running on it still queues up the 6th request, but because it uses async and await, each of its threads are freed up when the I/O-bound work starts, rather than when it finishes. Wenn die 20. Anforderung eingeht, ist die Warteschlange für eingehende Anforderungen weitaus kleiner (sofern sie überhaupt etwas enthält), und der Server wird nicht langsamer.By the time the 20th request comes in, the queue for incoming requests will be far smaller (if it has anything in it at all), and the server won't slow down.

Dies ist zwar ein erfundenes Beispiel, doch in der Praxis funktioniert es sehr ähnlich.Although this is a contrived example, it works in a very similar fashion in the real world. In der Tat können Sie erwarten, dass ein Server mit async und await bedeutend mehr Anforderungen verarbeiten kann, als wenn er einen Thread für jede empfangene Anforderung dediziert.In fact, you can expect a server to be able to handle an order of magnitude more requests using async and await than if it were dedicating a thread for each request it receives.

Was bedeutet dies für ein Clientszenario?What does this mean for client scenario?

Der größte Gewinn durch die Verwendung von async und await liegt für eine Client-App in der gesteigerten Reaktionsfähigkeit.The biggest gain for using async and await for a client app is an increase in responsiveness. Sie können die Reaktionsfähigkeit einer App zwar durch manuelles Erstellen von Threads begünstigen, doch im Vergleich mit der Verwendung von async und await ist das Erstellen von Threads ein aufwändiger Vorgang.Although you can make an app responsive by spawning threads manually, the act of spawning a thread is an expensive operation relative to just using async and await. Insbesondere für Anwendungen wie mobile Spiele ist eine minimale Auswirkung der E/A auf den Benutzeroberflächenthread entscheidend.Especially for something like a mobile game, impacting the UI thread as little as possible where I/O is concerned is crucial.

Noch wichtiger: Da E/A-gebundene Arbeit praktisch keine CPU-Zeit beansprucht, wäre das Dedizieren eines gesamten CPU-Threads, um kaum sinnvolle Arbeit durchzuführen, eine schlechte Ressourcennutzung.More importantly, because I/O-bound work spends virtually no time on the CPU, dedicating an entire CPU thread to perform barely any useful work would be a poor use of resources.

Außerdem ist das Verteilen von Arbeit an den Benutzeroberflächenthread (z.B. die Aktualisierung einer Benutzeroberfläche) mit async-Methoden sehr einfach und erfordert keine zusätzliche Arbeit (wie den Aufruf eines threadsicheren Delegaten).Additionally, dispatching work to the UI thread (such as updating a UI) is very simple with async methods, and does not require extra work (such as calling a thread-safe delegate).

Tieferer Einblick in Task und Task für einen CPU-gebundenen VorgangDeeper Dive into Task and Task for a CPU-Bound Operation

CPU-gebundener async-Code ist etwas anders als E/A-gebundener async-Code.CPU-bound async code is a bit different than I/O-bound async code. Da die Arbeit auf der CPU ausgeführt wird, besteht keine Möglichkeit, einen Thread für die Berechnung zu dedizieren.Because the work is done on the CPU, there's no way to get around dedicating a thread to the computation. Die Verwendung von async und await bietet Ihnen eine saubere Möglichkeit, mit einem Hintergrundthread zu interagieren und den Aufrufer der Async-Methode reaktionsfähig zu halten.The use of async and await provides you with a clean way to interact with a background thread and keep the caller of the async method responsive. Beachten Sie, dass dies keinen Schutz freigegebener Daten bietet.Note that this does not provide any protection for shared data. Wenn Sie freigegebene Daten verwenden, müssen Sie eine entsprechende Synchronisierungsstrategie anwenden.If you are using shared data, you will still need to apply an appropriate synchronization strategy.

Hier ist eine allgemeine Ansicht eines CPU-gebundenen asynchronen Aufrufs:Here's a 10,000 foot view of a CPU-bound async call:

public async Task<int> CalculateResult(InputData data)
{
    // This queues up the work on the threadpool.
    var expensiveResultTask = Task.Run(() => DoExpensiveCalculation(data));

    // Note that at this point, you can do some other work concurrently,
    // as CalculateResult() is still executing!

    // Execution of CalculateResult is yielded here!
    var result = await expensiveResultTask;

    return result;
}

CalculateResult() wird auf dem Thread aufgeführt, von dem der Aufruf erfolgte.CalculateResult() executes on the thread it was called on. Bei Aufruf von Task.Run wird der aufwändige CPU-gebundene Vorgang, DoExpensiveCalculation(), auf dem Threadpool in die Warteschlange gestellt und ein Task<int>-Handle empfangen.When it calls Task.Run, it queues the expensive CPU-bound operation, DoExpensiveCalculation(), on the thread pool and receives a Task<int> handle. DoExpensiveCalculation() wird schließlich gleichzeitig auf dem nächsten verfügbaren Thread ausgeführt, wahrscheinlich auf einen anderen CPU-Kern.DoExpensiveCalculation() is eventually run concurrently on the next available thread, likely on another CPU core. Es ist möglich, gleichzeitig Arbeit auszuführen, während DoExpensiveCalculation() auf einem anderen Thread aktiv ist, da der Thread, der CalculateResult() aufgerufen hat, immer noch ausgeführt wird.It's possible to do concurrent work while DoExpensiveCalculation() is busy on another thread, because the thread which called CalculateResult() is still executing.

Sobald await festgestellt wird, wird die Ausführung von CalculateResult() an den Aufrufer übergeben, sodass andere Arbeit mit dem aktuellen Thread erledigt werden kann, während DoExpensiveCalculation() ein Ergebnis zurückgibt.Once await is encountered, the execution of CalculateResult() is yielded to its caller, allowing other work to be done with the current thread while DoExpensiveCalculation() is churning out a result. Anschließend wird das Ergebnis in die Warteschlange gestellt, um im Hauptthread ausgeführt zu werden.Once it has finished, the result is queued up to run on the main thread. Schließlich kehrt der Hauptthread zur Ausführung von CalculateResult() zurück. An diesem Punkt hat er das Ergebnis von DoExpensiveCalculation().Eventually, the main thread will return to executing CalculateResult(), at which point it will have the result of DoExpensiveCalculation().

Warum ist Async hier hilfreich?Why does async help here?

async und await stellen die Best Practice zum Verwalten von CPU-gebundener Arbeit dar, wenn Sie Wert auf Reaktionsfähigkeit legen.async and await are the best practice managing CPU-bound work when you need responsiveness. Es gibt mehrere Muster zur Async-Verwendung mit CPU-gebundener Arbeit.There are multiple patterns for using async with CPU-bound work. Sie sollten unbedingt beachten, dass die Async-Verwendung mit geringem Kostenaufwand verbunden ist und nicht für enge Schleifen empfohlen wird.It's important to note that there is a small cost to using async and it's not recommended for tight loops. Sie entscheiden, wie Sie diese neue Funktion in Ihren Code einbringen.It's up to you to determine how you write your code around this new capability.

Siehe auchSee also

Asynchrone Programmierung in C# Asynchronous programming in C#
Asynchrone Programmierung in F# Async Programming in F#
Asynchrone Programmierung mit „Async“ und „Await“ (Visual Basic)Asynchronous Programming with Async and Await (Visual Basic)