Detaillierte C#-Funktionsprogrammierung (14) Asynchrone Funktion

Detaillierte C#-Funktionsprogrammierung (14) Asynchrone Funktion

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

Neueste Version:https://weblogs.asp.net/dixin/functional-csharp-asynchronous-function

Die asynchrone Funktion kann die Reaktionsfähigkeit und Skalierbarkeit der Anwendung und des Dienstes verbessern. C# 5.0 führt die Schlüsselwörter async und await ein, um das asynchrone Programmiermodell erheblich zu vereinfachen.

Task, Task und Asynchronität

Im asynchronen C#/.NET-Programmiermodell wird System.Threading.Tasks.Task bereitgestellt, um einen asynchronen Vorgang darzustellen, der void zurückgibt, und System.Threading.Tasks.Task wird bereitgestellt, um einen asynchronen Vorgang darzustellen, der den TResult-Wert zurückgibt:

namespace System.Threading.Tasks
{
    public partial class Task : IAsyncResult
    {
        public Task(Action action); // () –> void

        public void Start();

        public void Wait();

        public TaskStatus Status { get; } // Created, WaitingForActivation, WaitingToRun, Running, WaitingForChildrenToComplete, RanToCompletion, Canceled, Faulted.

        public bool IsCanceled { get; }

        public bool IsCompleted { get; }

        public bool IsFaulted { get; }

        public AggregateException Exception { get; }

        Task ContinueWith(Action<Task> continuationAction);

        Task<TResult> ContinueWith<TResult>(Func<Task, TResult> continuationFunction);

        // Other members.
    }

    public partial class Task<TResult> : Task
    {
        public Task(Func<TResult> function); // () –> TResult

        public TResult Result { get; }

        public Task ContinueWith(Action<Task<TResult>> continuationAction);

        public Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult> continuationFunction);

        // Other members.
    }
}

Task und Task können mit () –> void-Funktion und () –> TResult-Funktion konstruiert und durch Aufrufen der Start-Methode gestartet werden. Eine Aufgabe wird asynchron ausgeführt und blockiert den aktuellen Thread nicht. Sein Status kann durch die Eigenschaften Status, IsCanceled, IsCompleted, IsFaulted abgefragt werden. Auf eine Aufgabe kann gewartet werden, indem ihre Wait-Methode aufgerufen wird, die den aktuellen Thread blockiert, bis die Aufgabe erfolgreich abgeschlossen ist, fehlschlägt oder abgebrochen wird. Wenn für Task der zugrunde liegende asynchrone Vorgang erfolgreich abgeschlossen wurde, ist das Ergebnis über die Result-Eigenschaft verfügbar. Für Task oder Task schlägt der zugrunde liegende asynchrone Vorgang mit einer Ausnahme fehl, die Ausnahme ist über die Exception-Eigenschaft verfügbar. Eine Aufgabe kann mit einem anderen asynchronen Fortsetzungsvorgang verkettet werden, indem die ContinueWith-Methoden aufgerufen werden. Wenn die Ausführung der Aufgabe beendet ist, wird die angegebene Fortsetzung asynchron ausgeführt. Wenn die Ausführung der Aufgabe bereits beendet ist, wenn ihre ContinueWith-Methode aufgerufen wird, beginnt die angegebene Fortsetzung sofort mit der Ausführung. Das folgende Beispiel erstellt und startet eine Aufgabe zum Lesen einer Datei und verkettet eine weitere Folgeaufgabe zum Schreiben des Inhalts in eine andere Datei:

internal static partial class Functions
{
    internal static void CreateTask(string readPath, string writePath)
    {
        Thread.CurrentThread.ManagedThreadId.WriteLine(); // 10
        Task<string> task = new Task<string>(() =>
        {
            Thread.CurrentThread.ManagedThreadId.WriteLine(); // 8
            return File.ReadAllText(readPath);
        });
        task.Start();
        Task continuationTask = task.ContinueWith(antecedentTask =>
        {
            Thread.CurrentThread.ManagedThreadId.WriteLine(); // 9
            object.ReferenceEquals(antecedentTask, task).WriteLine(); // True
            if (antecedentTask.IsFaulted)
            {
                antecedentTask.Exception.WriteLine();
            }
            else
            {
                File.WriteAllText(writePath, antecedentTask.Result);
            }
        });
        continuationTask.Wait();
    }
}

Als asynchrone Vorgänge werden beim Starten von Aufgaben die umschlossenen Funktionen standardmäßig für die Ausführung im CLR/CoreCLR-Thread-Pool geplant, sodass sich ihre Thread-IDs von der aufrufenden Thread-ID unterscheiden.

Task bietet auch Run-Methoden zum Erstellen und automatischen Starten von Tasks:

namespace System.Threading.Tasks
{
    public partial class Task : IAsyncResult
    {
        public static Task Run(Action action);

        public static Task<TResult> Run<TResult>(Func<TResult> function);
    }
}

Vergleichen Sie nun die folgenden Funktionen:

internal static void Write(string path, string contents) => File.WriteAllText(path, contents);

internal static string Read(string path) => File.ReadAllText(path);

internal static Task WriteAsync(string path, string contents) => 
    Task.Run(() => File.WriteAllText(path, contents));

internal static Task<string> ReadAsync(string path) => Task.Run(() => File.ReadAllText(path));

Wenn Write aufgerufen wird, blockiert seine Ausführung den aktuellen Thread. Wenn der Schreibvorgang synchron ausgeführt wird, kehrt er ohne Ergebnis zurück, und der aufrufende Thread kann die Ausführung fortsetzen. Wenn Read aufgerufen wird, blockiert seine Ausführung in ähnlicher Weise auch den aktuellen Thread. Wenn der Lesevorgang synchron ausgeführt wird, gibt er das Ergebnis zurück, sodass das Ergebnis für den Aufrufer verfügbar ist und der Aufrufer mit der Ausführung fortfahren kann. Wenn WriteAsync aufgerufen wird, wird Task.Run aufgerufen, um eine Task-Instanz mit dem Schreibvorgang zu erstellen, die Aufgabe zu starten und die Aufgabe dann sofort zurückzugeben. Dann kann der Aufrufer fortfahren, ohne durch die Ausführung des Schreibvorgangs blockiert zu werden. Standardmäßig wird der Schreibvorgang für den Thread-Pool geplant, wenn er abgeschlossen ist, gibt der Schreibvorgang kein Ergebnis zurück und der Status der Aufgabe wird aktualisiert. Ebenso wird beim Aufruf von ReadAsync auch Task.Run aufgerufen, um eine Task-Instanz mit dem Lesevorgang zu erstellen, die Aufgabe zu starten und die Aufgabe dann sofort zurückzugeben. Dann kann der Aufrufer fortfahren, ohne durch die Ausführung des Lesevorgangs blockiert zu werden. Standardmäßig wird der Lesevorgang auch für den Thread-Pool geplant, wenn er abgeschlossen ist, hat der Lesevorgang ein Ergebnis und der Status der Aufgabe wird aktualisiert, wobei das Ergebnis über die Eigenschaft Result verfügbar ist.

internal static void CallReadWrite(string path, string contents)
{
    Write(path, contents); // Blocking.
    // Sync operation is completed with no result.
    string result = Read(path); // Blocking.
    // Sync operation is completed with result available.

    Task writeTask = WriteAsync(path, contents); // Non blocking.
    // Async operation is scheduled to thread pool, and will be completed in the future with no result.
    Task<string> readTask = ReadAsync(path); // Non blocking.
    // Async operation is scheduled to thread pool, and will be completed in the future, then result will be available.
}

Write, das void zurückgibt, und Read, das ein Ergebnis zurückgibt, sind also Sync-Funktionen. WriteAsync, das Task zurückgibt, und ReadAsync, das Task zurückgibt, sind asynchrone Funktionen, bei denen Task als zukünftiges Void und Task als zukünftiges TResult-Ergebnis angezeigt werden kann. Hier werden WriteAsync und ReadAsync asynchron, indem die Vorgänge einfach in den Threadpool ausgelagert werden. Dies dient zu Demonstrationszwecken und bringt keine Verbesserung der Skalierbarkeit. Eine bessere Implementierung wird später diskutiert.

Benannte asynchrone Funktion

Standardmäßig gibt die benannte asynchrone Funktion „Task“ oder „Task“ zurück und hat als Konvention das Postfix „Async“ oder „AsyncTask“ im Namen. Das folgende Beispiel ist ein Lese- und Schreibworkflow für Synchronisierungsfunktionsaufrufe:

internal static void ReadWrite(string readPath, string writePath)
{
    string contents = Read(readPath);
    Write(writePath, contents);
}

Dieselbe Logik kann durch Aufrufen der asynchronen Version von Funktionen implementiert werden:

internal static async Task ReadWriteAsync(string readPath, string writePath)
{
    string contents = await ReadAsync(readPath);
    await WriteAsync(writePath, contents);
}

Hier wird await für jeden asynchronen Funktionsaufruf verwendet, und die Codestruktur bleibt die gleiche wie beim Synchronisierungsworkflow. Wenn das Schlüsselwort await im Funktionstext verwendet wird, ist der async-Modifizierer für diese Funktion erforderlich. In Bezug auf den Workflow, der kein Ergebnis zurückgibt, gibt die asynchrone Funktion Task (future void) zurück. Diese ReadWriteAsync-Funktion ruft asynchrone Funktionen auf und ist selbst auch eine asynchrone Funktion, da sie über den async-Modifizierer verfügt und Task zurückgibt. Wenn ReadWriteAsync aufgerufen wird, funktioniert es genauso wie ReadAsync und WriteAsync. Es blockiert seinen Aufrufer nicht und gibt sofort eine Aufgabe zurück, um den geplanten Lese- und Schreib-Workflow darzustellen.

Das await-Schlüsselwort kann also so angesehen werden, dass es praktisch darauf wartet, dass die zugrunde liegende asynchrone Operation der Aufgabe abgeschlossen wird. Wenn die Aufgabe fehlschlägt, wird eine Ausnahme ausgelöst. Wenn die Aufgabe erfolgreich abgeschlossen wurde, wird die Fortsetzung direkt nach dem await-Ausdruck zurückgerufen. Wenn die Aufgabe ein Ergebnis hat, kann await das Ergebnis extrahieren. Daher sieht der asynchrone Workflow genauso aus wie der Synchronisierungsworkflow. Es ist kein ContinueWith-Aufruf erforderlich, um die Fortsetzung zu erstellen. Das folgende Beispiel ist ein komplexerer Datenbankabfrage-Workflow mit Synchronisierungsfunktionsaufrufen, und als Abfrageergebnis wird ein int-Wert zurückgegeben:

internal static int Query(DbConnection connection, StreamWriter logWriter)
{
    try
    {
        connection.Open(); // Return void.
        using (DbCommand command = connection.CreateCommand())
        {
            command.CommandText = "SELECT 1;";
            using (DbDataReader reader = command.ExecuteReader()) // Return DbDataReader.
            {
                if (reader.Read()) // Return bool.
                {
                    return (int)reader[0];
                }
                throw new InvalidOperationException("Failed to call sync functions.");
            }
        }
    }
    catch (SqlException exception)
    {
        logWriter.WriteLine(exception.ToString()); // Return void.
        throw new InvalidOperationException("Failed to call sync functions.", exception);
    }
}

Hier haben die Methoden DbConnection.Open, DbCommand.ExecuteReader, DbDataReader.Read, StreamWriter.WriteLine eine asynchrone Version, die als DbConnection.OpenAsync, DbCommand.ExecuteReaderAsync, DbDataReader.ReadAsync, StreamWriter.WriteLineAsync bereitgestellt wird. Sie geben entweder Task oder Task zurück. Mit den Schlüsselwörtern async und await ist es einfach, diese asynchronen Funktionen aufzurufen:

internal static async Task<int> QueryAsync(DbConnection connection, StreamWriter logWriter)
{
    try
    {
        await connection.OpenAsync(); // Return Task.
        using (DbCommand command = connection.CreateCommand())
        {
            command.CommandText = "SELECT 1;";
            using (DbDataReader reader = await command.ExecuteReaderAsync()) // Return Task<DbDataReader>.
            {
                if (await reader.ReadAsync()) // Return Task<bool>.
                {
                    return (int)reader[0];
                }
                throw new InvalidOperationException("Failed to call async functions.");
            }
        }
    }
    catch (SqlException exception)
    {
        await logWriter.WriteLineAsync(exception.ToString()); // Return Task.
        throw new InvalidOperationException("Failed to call async functions.", exception);
    }
}

Auch hier behält der asynchrone Workflow die gleiche Codestruktur bei wie der Synchronisierungsworkflow, der Try-Catch, der verwendet, wenn der Block gleich aussieht. Ohne diese Syntax ist es viel komplexer, ContinueWith aufzurufen und den obigen Workflow manuell zu erstellen. In Bezug auf die asynchrone Funktion, die ein int-Ergebnis zurückgibt, ist ihr Rückgabetyp Task (future int).

Die obigen Write- und Read-Funktionen rufen File.WriteAllText und File.ReadAllText auf, um einen Synchronisierungs-E/A-Vorgang auszuführen, der intern durch Aufrufen von StreamWriter.Write und StreamReader.ReadToEnd implementiert wird. Mit den Schlüsselwörtern async und await können WriteAsync und ReadAsync jetzt als echte asynchrone E/A implementiert werden (solange das zugrunde liegende Betriebssystem asynchrone E/A unterstützt), indem StreamWriter.WriteAsync und StreamReader.ReadToEndAsync aufgerufen werden:

internal static async Task WriteAsync(string path, string contents)
{
    // File.WriteAllText:
    // using (StreamWriter writer = new StreamWriter(new FileStream(
    //    path: path, mode: FileMode.Create, access: FileAccess.Write,
    //    share: FileShare.Read, bufferSize: 4096, useAsync: false)))
    // {
    //    writer.Write(contents);
    // }
    using (StreamWriter writer = new StreamWriter(new FileStream(
        path: path, mode: FileMode.Create, access: FileAccess.Write,
        share: FileShare.Read, bufferSize: 4096, useAsync: true)))
    {
        await writer.WriteAsync(contents);
    }
}

internal static async Task<string> ReadAsync(string path)
{
    // File.ReadAllText:
    // using (StreamReader reader = new StreamReader(new FileStream(
    //    path: path, mode: FileMode.Open, access: FileAccess.Read, 
    //    share: FileShare.Read, bufferSize: 4096, useAsync: false)))
    // {
    //    return reader.ReadToEnd();
    // }
    using (StreamReader reader = new StreamReader(new FileStream(
        path: path, mode: FileMode.Open, access: FileAccess.Read, 
        share: FileShare.Read, bufferSize: 4096, useAsync: true)))
    {
        return await reader.ReadToEndAsync();
    }
}

Es gibt ein spezielles Szenario, in dem die asynchrone Funktion void anstelle von Task zurückgeben muss – der asynchrone Ereignishandler. Beispielsweise hat ObservableCollection ein CollectionChanged-Ereignis:

namespace System.Collections.ObjectModel
{
    public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
    {
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        // Other members.
    }
}

namespace System.Collections.Specialized
{
    public delegate void NotifyCollectionChangedEventHandler(object sender, NotifyCollectionChangedEventArgs e);
}

Dieses Ereignis erfordert, dass sein Handler eine Funktion vom Typ (Objekt, NotifyCollectionChangedEventArgs) –> void ist. Wenn Sie also eine asynchrone Funktion als Handler des obigen Ereignisses definieren, muss diese asynchrone Funktion void anstelle von Task:

zurückgeben
internal static partial class Functions
{
    private static StringBuilder logs = new StringBuilder();

    private static StringWriter logWriter = new StringWriter(logs);

    private static async void CollectionChangedAsync(object sender, NotifyCollectionChangedEventArgs e) =>
        await logWriter.WriteLineAsync(e.Action.ToString());

    internal static void EventHandler()
    {
        ObservableCollection<int> collection = new ObservableCollection<int>();
        collection.CollectionChanged += CollectionChangedAsync;
        collection.Add(1); // Fires CollectionChanged event.
    }
}

Neben der von den asynchronen Funktionen zurückgegebenen Aufgabe funktioniert das Schlüsselwort await mit jeder Task- und Task-Instanz:

internal static async Task AwaitTasks(string path)
{
    // string contents = await ReadAsync(path);
    Task<string> task1 = ReadAsync(path);
    string contents = await task1;

    // await WriteAsync(path, contents);
    Task task2 = WriteAsync(path, contents);
    await task2;

    // await Task.Run(() => { });
    Task task3 = Task.Run(() => { });
    await task3;

    // int result = await Task.Run(() => 0);
    Task<int> task4 = Task.Run(() => 0);
    int result = await task4;

    // await Task.Delay(TimeSpan.FromSeconds(10));
    Task task5 = Task.Delay(TimeSpan.FromSeconds(10));
    await task5;

    // result = await Task.FromResult(result);
    Task<int> task6 = Task.FromResult(result);
    result = await task6;
}

Wenn eine Aufgabe nie gestartet wird, wird sie nie beendet. Der Code nach seinem await-Ausdruck wird nie zurückgerufen:

internal static async Task HotColdTasks(string path)
{
    Task hotTask = new Task(() => { });
    hotTask.Start();
    await hotTask;
    hotTask.Status.WriteLine();

    Task coldTask = new Task(() => { });
    await coldTask;
    coldTask.Status.WriteLine(); // Never executes.
}

Eine noch nicht gestartete Aufgabe wird als kalte Aufgabe bezeichnet, und eine bereits gestartete Aufgabe wird als heiße Aufgabe bezeichnet. Als Konvention sollte jede Funktion, die eine Aufgabe zurückgibt, immer eine heiße Aufgabe zurückgeben. Alle .NET-APIs folgen dieser Konvention.

Awaitable-awaiter-Muster

C# kompiliert den await-Ausdruck mit dem awaitable-awaiter-Muster. Neben Task und Task kann das Schlüsselwort await mit jedem erwartungsfähigen Typ verwendet werden. Ein Awaiter-Typ hat eine GetAwaiter-Instanz oder eine Erweiterungsmethode, um einen Awaiter zurückzugeben. Ein awaiter-Typ implementiert die System.Runtime.CompilerServices.INotifyCompletion-Schnittstelle, hat auch eine IsCompleted-Eigenschaft, die einen booleschen Wert zurückgibt, und eine GetResult-Instanzmethode, die entweder void oder einen Ergebniswert zurückgibt. Die folgenden IAwaitable- und IAwaiter-Schnittstellen demonstrieren das Awaitable-Awaiter-Muster für Operationen ohne Ergebnis:

public interface IAwaitable
{
    IAwaiter GetAwaiter();
}

public interface IAwaiter : INotifyCompletion
{
    bool IsCompleted { get; }

    void GetResult(); // No result.
}

Und die folgenden IAwaitable- und IAwaiter-Schnittstellen demonstrieren das Awaitable-Waiter-Muster für Operationen mit einem Ergebnis:

public interface IAwaitable<TResult>
{
    IAwaiter<TResult> GetAwaiter();
}

public interface IAwaiter<TResult> : INotifyCompletion
{
    bool IsCompleted { get; }

    TResult GetResult(); // TResult result.
}

Und die INotifyCompletion-Schnittstelle hat eine einzige OnCompleted-Methode, um eine Fortsetzung zu verketten:

namespace System.Runtime.CompilerServices
{
    public interface INotifyCompletion
    {
        void OnCompleted(Action continuation);
    }
}

So implementieren Task und Task das Awaitable-Awaiter-Muster. Task kann virtuell als Implementierung von IAwaiter angesehen werden, es hat eine GetAwaiter-Instanzmethode, die System.Runtime.CompilerServices.TaskAwaiter zurückgibt, was virtuell als Implementierung von IAwaiter angesehen werden kann; Ebenso kann Task virtuell als Implementierung von IAwaitable angesehen werden, es hat eine GetAwaiter-Methode, die System.Runtime.CompilerServices.TaskAwaiter zurückgibt, was virtuell als Implementierung von IAwaiter angesehen werden kann:

namespace System.Threading.Tasks
{
    public partial class Task : IAsyncResult
    {
        public TaskAwaiter GetAwaiter();
    }

    public partial class Task<TResult> : Task
    {
        public TaskAwaiter<TResult> GetAwaiter();
    }
}

namespace System.Runtime.CompilerServices
{
    public struct TaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
    {
        public bool IsCompleted { get; }

        public void GetResult(); // No result.

        public void OnCompleted(Action continuation);

        // Other members.
    }

    public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion, INotifyCompletion
    {
        public bool IsCompleted { get; }

        public TResult GetResult(); // TResult result.

        public void OnCompleted(Action continuation);

        // Other members.
    }
}

Jeder andere Typ kann mit dem await-Schlüsselwort verwendet werden, solange das awaitable-awaiter-Muster implementiert ist. Nehmen Sie als Beispiel Action, eine GetAwaiter-Methode kann einfach als Erweiterungsmethode implementiert werden, indem Sie den obigen TaskAwaiter wiederverwenden:

public static partial class ActionExtensions
{
    public static TaskAwaiter GetAwaiter(this Action action) => Task.Run(action).GetAwaiter();
}

Auf ähnliche Weise kann dieses Muster für Func implementiert werden, indem TaskAwaiter:

wiederverwendet wird
public static partial class FuncExtensions
{
    public static TaskAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function) =>
        Task.Run(function).GetAwaiter();
}

Jetzt kann das Schlüsselwort await direkt mit einer Funktion verwendet werden:

internal static async Task AwaitFunctions(string readPath, string writePath)
{
    Func<string> read = () => File.ReadAllText(readPath);
    string contents = await read;

    Action write = () => File.WriteAllText(writePath, contents);
    await write;
}

Asynchrone Zustandsmaschine

Wie bereits erwähnt, blockiert eine asynchrone Funktion mit den Schlüsselwörtern async und await nicht. Zur Kompilierzeit wird der Arbeitsablauf einer asynchronen Funktion in einen asynchronen Zustandsautomaten kompiliert. Wenn diese asynchrone Funktion zur Laufzeit aufgerufen wird, startet sie einfach die vom Compiler generierte asynchrone Zustandsmaschine und gibt sofort eine Aufgabe zurück, die den Workflow in der asynchronen Zustandsmaschine darstellt. Um dies zu demonstrieren, definieren Sie die folgenden asynchronen Methoden:

internal static async Task<T> Async<T>(T value)
{
    T value1 = Start(value);
    T result1 = await Async1(value1);
    T value2 = Continuation1(result1);
    T result2 = await Async2(value2);
    T value3 = Continuation2(result2);
    T result3 = await Async3(value3);
    T result = Continuation3(result3);
    return result;
}

internal static T Start<T>(T value) => value;

internal static Task<T> Async1<T>(T value) => Task.Run(() => value);

internal static T Continuation1<T>(T value) => value;

internal static Task<T> Async2<T>(T value) => Task.FromResult(value);

internal static T Continuation2<T>(T value) => value;

internal static Task<T> Async3<T>(T value) => Task.Run(() => value);

internal static T Continuation3<T>(T value) => value;

Nach der Kompilierung ist der async-Modifizierer weg. Die asynchrone Funktion wird zu einer normalen Funktion zum Starten einer asynchronen Zustandsmaschine:

[AsyncStateMachine(typeof(AsyncStateMachine<>))]
internal static Task<T> CompiledAsync<T>(T value)
{
    AsyncStateMachine<T> asyncStateMachine = new AsyncStateMachine<T>()
    {
        Value = value,
        Builder = AsyncTaskMethodBuilder<T>.Create(),
        State = -1 // -1 means start.
    };
    asyncStateMachine.Builder.Start(ref asyncStateMachine);
    return asyncStateMachine.Builder.Task;
}

Und die generierte asynchrone Zustandsmaschine ist eine Struktur im Release-Build und eine Klasse im Debug-Build:

[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
private struct AsyncStateMachine<TResult> : IAsyncStateMachine
{
    public int State;

    public AsyncTaskMethodBuilder<TResult> Builder;

    public TResult Value;

    private TaskAwaiter<TResult> awaiter;

    void IAsyncStateMachine.MoveNext()
    {
        TResult result;
        try
        {
            switch (this.State)
            {
                case -1: // Start code from the beginning to the 1st await.
                    // Workflow begins.
                    TResult value1 = Start(this.Value);
                    this.awaiter = Async1(value1).GetAwaiter();
                    if (this.awaiter.IsCompleted)
                    {
                        // If the task returned by Async1 is already completed, immediately execute the continuation.
                        goto case 0;
                    }
                    else
                    {
                        this.State = 0;
                        // If the task returned by Async1 is not completed, specify the continuation as its callback.
                        this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
                        // Later when the task returned by Async1 is completed, it calls back MoveNext, where State is 0.
                        return;
                    }
                case 0: // Continuation code from after the 1st await to the 2nd await.
                    // The task returned by Async1 is completed. The result is available immediately through GetResult.
                    TResult result1 = this.awaiter.GetResult();
                    TResult value2 = Continuation1(result1);
                    this.awaiter = Async2(value2).GetAwaiter();
                    if (this.awaiter.IsCompleted)
                    {
                        // If the task returned by Async2 is already completed, immediately execute the continuation.
                        goto case 1;
                    }
                    else
                    {
                        this.State = 1;
                        // If the task returned by Async2 is not completed, specify the continuation as its callback.
                        this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
                        // Later when the task returned by Async2 is completed, it calls back MoveNext, where State is 1.
                        return;
                    }
                case 1: // Continuation code from after the 2nd await to the 3rd await.
                    // The task returned by Async2 is completed. The result is available immediately through GetResult.
                    TResult result2 = this.awaiter.GetResult();
                    TResult value3 = Continuation2(result2);
                    this.awaiter = Async3(value3).GetAwaiter();
                    if (this.awaiter.IsCompleted)
                    {
                        // If the task returned by Async3 is already completed, immediately execute the continuation.
                        goto case 2;
                    }
                    else
                    {
                        this.State = 2;
                        // If the task returned by Async3 is not completed, specify the continuation as its callback.
                        this.Builder.AwaitUnsafeOnCompleted(ref this.awaiter, ref this);
                        // Later when the task returned by Async3 is completed, it calls back MoveNext, where State is 1.
                        return;
                    }
                case 2: // Continuation code from after the 3rd await to the end.
                    // The task returned by Async3 is completed. The result is available immediately through GetResult.
                    TResult result3 = this.awaiter.GetResult();
                    result = Continuation3(result3);
                    this.State = -2; // -2 means end.
                    this.Builder.SetResult(result);
                    // Workflow ends.
                    return;
            }
        }
        catch (Exception exception)
        {
            this.State = -2; // -2 means end.
            this.Builder.SetException(exception);
        }
    }

    [DebuggerHidden]
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine asyncStateMachine) =>
        this.Builder.SetStateMachine(asyncStateMachine);
}

Der generierte asynchrone Zustandsautomat ist ein endlicher Zustandsautomat:

Der Workflow wird in seine MoveNext-Methode kompiliert, und der Workflow wird durch die 3 await-Schlüsselwörter in 4 Blöcke aufgeteilt. Der Parameter des Workflows wird als Feld der Zustandsmaschine kompiliert, sodass der Workflow innerhalb von MoveNext darauf zugreifen kann. Wenn die Zustandsmaschine initialisiert wird, ist ihr Anfangszustand –1, was Start bedeutet. Sobald der Zustandsautomat gestartet ist, wird MoveNext aufgerufen und der case –1-Block ausgeführt, der den Code vom Beginn des Workflows bis zum ersten await-Ausdruck enthält, der zu einem GetAwaiter-Aufruf kompiliert wird. Wenn der Erwartete bereits abgeschlossen ist, sollte die Fortsetzung sofort ausgeführt werden, damit der nächste Fall-0-Block ausgeführt wird; Wenn der Erwartete nicht abgeschlossen ist, wird die Fortsetzung (MoveNext-Aufruf mit dem nächsten Zustand 0) als Rückruf des Erwarteten angegeben, wenn er in der Zukunft abgeschlossen wird. In beiden Fällen ist der vorherige Awaiter bereits abgeschlossen, wenn der Code im Fall 0-Block ausgeführt wird, und sein Ergebnis ist sofort über seine GetResult-Methode verfügbar. Die Ausführung geht nach dem gleichen Muster weiter, bis der letzte Block von Fall 2 ausgeführt ist.

Kontexterfassung zur Laufzeit

Wenn die erwartete Aufgabe noch nicht abgeschlossen ist, wird für jeden Erwartungsausdruck die Fortsetzung als Rückruf geplant, wenn sie abgeschlossen ist. Infolgedessen kann die Fortsetzung von einem Thread ausgeführt werden, der sich vom ursprünglichen Aufrufer-Thread unterscheidet. Standardmäßig werden die Laufzeitkontextinformationen des anfänglichen Threads erfasst und von der wiederverwendet, um die Fortsetzung auszuführen. Um dies zu demonstrieren, kann das obige Awaitable-Awaiter-Muster für Action mit custom awaiter neu implementiert werden:

public static partial class ActionExtensions
{
    public static IAwaiter GetAwaiter(this Action action) => new ActionAwaiter(Task.Run(action));
}

public class ActionAwaiter : IAwaiter
{
    private readonly (SynchronizationContext, TaskScheduler, ExecutionContext) runtimeContext =
        RuntimeContext.Capture();

    private readonly Task task;

    public ActionAwaiter(Task task) => this.task = task;

    public bool IsCompleted => this.task.IsCompleted;

    public void GetResult() => this.task.Wait();

    public void OnCompleted(Action continuation) => this.task.ContinueWith(task =>
        this.runtimeContext.Execute(continuation));
}

Wenn der Awaiter erstellt wird, erfasst er die Laufzeitkontextinformationen, einschließlich System.Threading.SynchronizationContext, System.Threading.Tasks.TaskScheduler und System.Threading.ExecutionContext des aktuellen Threads. Wenn die Fortsetzung dann in OnCompleted zurückgerufen wird, wird sie mit den zuvor erfassten Laufzeitkontextinformationen ausgeführt. Der benutzerdefinierte Awaiter kann für Func nach demselben Muster implementiert werden:

public static partial class FuncExtensions
{
    public static IAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function) =>
        new FuncAwaiter<TResult>(Task.Run(function));
}

public class FuncAwaiter<TResult> : IAwaiter<TResult>
{
    private readonly (SynchronizationContext, TaskScheduler, ExecutionContext) runtimeContext =
        RuntimeContext.Capture();

    private readonly Task<TResult> task;

    public FuncAwaiter(Task<TResult> task) => this.task = task;

    public bool IsCompleted => this.task.IsCompleted;

    public TResult GetResult() => this.task.Result;

    public void OnCompleted(Action continuation) => this.task.ContinueWith(task =>
        this.runtimeContext.Execute(continuation));
}

Das Folgende ist eine grundlegende Implementierung der Laufzeitkontexterfassung und -fortsetzung:

public static class RuntimeContext
{
    public static (SynchronizationContext, TaskScheduler, ExecutionContext) Capture() =>
        (SynchronizationContext.Current, TaskScheduler.Current, ExecutionContext.Capture());

    public static void Execute(
        this (SynchronizationContext, TaskScheduler, ExecutionContext) runtimeContext, Action continuation)
    {
        var (synchronizationContext, taskScheduler, executionContext) = runtimeContext;
        if (synchronizationContext != null && synchronizationContext.GetType() != typeof(SynchronizationContext))
        {
            if (synchronizationContext == SynchronizationContext.Current)
            {
                executionContext.Run(continuation);
            }
            else
            {
                executionContext.Run(() => synchronizationContext.Post(
                    d: state => continuation(), state: null));
            }
            return;
        }
        if (taskScheduler != null && taskScheduler != TaskScheduler.Default)
        {
            Task continuationTask = new Task(continuation);
            continuationTask.Start(taskScheduler);
            return;
        }
        executionContext.Run(continuation);
    }

    public static void Run(this ExecutionContext executionContext, Action continuation)
    {
        if (executionContext != null)
        {
            ExecutionContext.Run(
                executionContext: executionContext, 
                callback: executionContextState => continuation(), 
                state: null);
        }
        else
        {
            continuation();
        }
    }
}

Beim Ausführen der Fortsetzung wird zunächst der zuvor erfasste Synchronisationskontext überprüft. Wenn ein spezialisierter Synchronisierungskontext erfasst wird und sich vom aktuellen Synchronisierungskontext unterscheidet, wird die Fortsetzung mit dem erfassten Synchronisierungskontext und Ausführungskontext ausgeführt. Wenn kein spezialisierter Synchronisationskontext erfasst wird, wird der TaskScheduler überprüft. Wenn ein spezialisierter TaskScheduler erfasst wird, wird er verwendet, um die Fortsetzung als Aufgabe zu planen. In allen anderen Fällen wird die Fortsetzung mit dem erfassten Ausführungskontext ausgeführt.

Task und Task stellen eine ConfigureAwait-Methode bereit, um anzugeben, ob die Fortsetzung in den zuvor erfassten Laufzeitkontext gemarshallt wird:

namespace System.Threading.Tasks
{
    public partial class Task : IAsyncResult
    {
        public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
    }

    public partial class Task<TResult> : Task
    {
        public ConfiguredTaskAwaitable<TResult> ConfigureAwait(bool continueOnCapturedContext);
    }
}

Um die Erfassung des Laufzeitkontexts zu demonstrieren, definieren Sie einen benutzerdefinierten Aufgabenplaner, der einfach einen Hintergrundthread startet, um jede Aufgabe auszuführen:

public class BackgroundThreadTaskScheduler : TaskScheduler
{
    protected override IEnumerable<Task> GetScheduledTasks() => throw new NotImplementedException();

    protected override void QueueTask(Task task) =>
        new Thread(() => this.TryExecuteTask(task)) { IsBackground = true }.Start();

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) =>
        this.TryExecuteTask(task);
}

Die folgende asynchrone Funktion hat 2 await-Ausdrücke, wobei ConfigureAwait mit unterschiedlichen bool-Werten aufgerufen wird:

internal static async Task ConfigureRuntimeContextCapture(string readPath, string writePath)
{
    TaskScheduler taskScheduler1 = TaskScheduler.Current;
    string contents = await ReadAsync(readPath).ConfigureAwait(continueOnCapturedContext: true);
    // Equivalent to: await ReadAsync(readPath);

    // Continuation is executed with captured runtime context.
    TaskScheduler taskScheduler2 = TaskScheduler.Current;
    object.ReferenceEquals(taskScheduler1, taskScheduler2).WriteLine(); // True
    await WriteAsync(writePath, contents).ConfigureAwait(continueOnCapturedContext: false);

    // Continuation is executed without captured runtime context.
    TaskScheduler taskScheduler3 = TaskScheduler.Current;
    object.ReferenceEquals(taskScheduler1, taskScheduler3).WriteLine(); // False
}

Rufen Sie zur Demonstration der Aufgabenplanererfassung die obige asynchrone Funktion auf, indem Sie den benutzerdefinierten Aufgabenplaner angeben:

internal static async Task CallConfigureContextCapture(string readPath, string writePath)
{
    Task<Task> task = new Task<Task>(() => ConfigureRuntimeContextCapture(readPath, writePath));
    task.Start(new BackgroundThreadTaskScheduler());
    await task.Unwrap(); // Equivalent to: await await task;
}

Da hier die asynchrone Funktion ConfigureRuntimeContextCapture Task zurückgibt, ist die mit der asynchronen Funktion erstellte Aufgabe vom Typ Task. Für Task wird eine Unwrap-Erweiterungsmethode bereitgestellt, um sie in eine normale Task:

umzuwandeln
namespace System.Threading.Tasks
{
    public static class TaskExtensions
    {
        public static Task Unwrap(this Task<Task> task);

        public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task);
    }
}

Wenn die asynchrone Funktion ConfigureRuntimeContextCapture ausgeführt wird, ist ihr anfänglicher Aufgabenplaner der angegebene benutzerdefinierte Aufgabenplaner. Im ersten await-Ausdruck wird ConfigureAwait mit true aufgerufen, sodass die Laufzeitkontextinformationen erfasst werden und die Fortsetzung mit den erfassten Laufzeitkontextinformationen ausgeführt wird. Dies ist das Standardverhalten, sodass das Aufrufen von ConfigureAwait mit „true“ gleichbedeutend damit ist, ConfigureAwait überhaupt nicht aufzurufen. Als Ergebnis wird die erste Fortsetzung mit demselben benutzerdefinierten Aufgabenplaner ausgeführt. Im zweiten await-Ausdruck wird ConfigureAwait mit false aufgerufen, sodass die Laufzeitkontextinformationen nicht erfasst werden. Als Ergebnis wird die zweite Fortsetzung mit dem Standard-Task-Scheduler (System.Threading.Tasks.ThreadPoolTaskScheduler) ausgeführt.

Die Laufzeitkontexterfassung kann auch durch SynchronizationContext demonstriert werden. SynchronizationContext hat verschiedene Implementierungen in verschiedenen Anwendungsmodellen, zum Beispiel:

  • ASP.NET:System.Web.AspNetSynchronizationContext
  • WPF:System.Windows.Threading.DispatcherSynchronizationContext
  • WinForms:System.Windows.Forms.WindowsFormsSynchronizationContext
  • WinRT und Windows Universal:System.Threading.WinRTSynchronizationContext

Nehmen Sie als Beispiel die universelle Windows-Anwendung. Erstellen Sie in Visual Studio eine universelle Windows-Anwendung und fügen Sie ihrer Benutzeroberfläche eine Schaltfläche hinzu:

<Button x:Name="Button" Content="Button" HorizontalAlignment="Center" VerticalAlignment="Center" Click="ButtonClick" />

Implementieren Sie im Code dahinter den Click-Ereignishandler als asynchrone Funktion:

private async void ButtonClick(object sender, RoutedEventArgs e)
{
    SynchronizationContext synchronizationContext1 = SynchronizationContext.Current;
    ExecutionContext executionContext1 = ExecutionContext.Capture();
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(continueOnCapturedContext: true);
    // Equivalent to: await Task.Delay(TimeSpan.FromSeconds(1));
            
    // Continuation is executed with captured runtime context.
    SynchronizationContext synchronizationContext2 = SynchronizationContext.Current;
    Debug.WriteLine(synchronizationContext1 == synchronizationContext2); // True
    this.Button.Background = new SolidColorBrush(Colors.Blue); // UI update works.
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(continueOnCapturedContext: false);
            
    // Continuation is executed without captured runtime context.
    SynchronizationContext synchronizationContext3 = SynchronizationContext.Current;
    Debug.WriteLine(synchronizationContext1 == synchronizationContext3); // False
    this.Button.Background = new SolidColorBrush(Colors.Yellow); // UI update fails.
    // Exception: The application called an interface that was marshalled for a different thread.
}

Der WinRTSynchronizationContext ist nur für den UI-Thread verfügbar. Wenn auf die Schaltfläche geklickt wird, führt der UI-Thread die asynchrone Funktion ButtonClick aus, sodass der anfängliche SynchronizationContext WinRTSynchronizationContext ist. Ähnlich wie im vorherigen Beispiel wird die Fortsetzung mit dem zuvor erfassten WinRTSynchronizationContext ausgeführt, wenn ConfigureAwait mit „true“ aufgerufen wird, sodass die Fortsetzung die Benutzeroberfläche erfolgreich aktualisieren kann. Wenn ConfigureAwait mit „true“ aufgerufen wird, wird die Fortsetzung nicht mit dem WinRTSynchronizationContext ausgeführt, und die Benutzeroberfläche kann nicht aktualisiert werden, und es wird eine Ausnahme ausgelöst.

Verallgemeinerter asynchroner Rückgabetyp und Generator für asynchrone Methoden

Seit C# 7 wird die asynchrone Funktion unterstützt, um jeden erwartebaren Typ zurückzugeben, solange ein asynchroner Methodengenerator angegeben ist. Zum Beispiel ist das folgende FuncAwaitable ein Awaitable-Typ, es verwendet oben FuncAwater als seinen Awaiter wieder:

[AsyncMethodBuilder(typeof(AsyncFuncAwaitableMethodBuilder<>))]
public class FuncAwaitable<TResult> : IAwaitable<TResult>
{
    private readonly Func<TResult> function;

    public FuncAwaitable(Func<TResult> function) => this.function = function;

    public IAwaiter<TResult> GetAwaiter() => new FuncAwaiter<TResult>(Task.Run(this.function));
}

Func ist bereits mit der obigen GetAwaiter-Erweiterungsmethode zu erwarten, aber hier ist ein solcher Wrapper-Typ implementiert, sodass ein asynchroner Methodenersteller dafür mit einem [AsyncMethodBuilder]-Attribut angegeben werden kann. Der Builder für asynchrone Methoden ist wie folgt definiert:

public class AsyncFuncAwaitableMethodBuilder<TResult>
{
    private AsyncTaskMethodBuilder<TResult> taskMethodBuilder;

    private TResult result;

    private bool hasResult;

    private bool useBuilder;

    public static AsyncFuncAwaitableMethodBuilder<TResult> Create() =>
        new AsyncFuncAwaitableMethodBuilder<TResult>()
        {
            taskMethodBuilder = AsyncTaskMethodBuilder<TResult>.Create()
        };

    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine =>
        this.taskMethodBuilder.Start(ref stateMachine);

    public void SetStateMachine(IAsyncStateMachine stateMachine) =>
        this.taskMethodBuilder.SetStateMachine(stateMachine);

    public void SetResult(TResult result)
    {
        if (this.useBuilder)
        {
            this.taskMethodBuilder.SetResult(result);
        }
        else
        {
            this.result = result;
            this.hasResult = true;
        }
    }

    public void SetException(Exception exception) => this.taskMethodBuilder.SetException(exception);

    public FuncAwaitable<TResult> Task
    {
        get
        {
            if (this.hasResult)
            {
                TResult result = this.result;
                return new FuncAwaitable<TResult>(() => result);
            }
            else
            {
                this.useBuilder = true;
                Task<TResult> task = this.taskMethodBuilder.Task;
                return new FuncAwaitable<TResult>(() => task.Result);
            }
        }
    }

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine
    {
        this.useBuilder = true;
        this.taskMethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
        ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine
    {
        this.useBuilder = true;
        this.taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    }
}

Jetzt kann der Typ FuncAwitable von der asynchronen Funktion zurückgegeben werden:

internal static async FuncAwaitable<T> ReturnFuncAwaitable<T>(T value)
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return value;
}

Seine Kompilierung erfolgt nach dem gleichen Muster wie die Aufgabe, die eine asynchrone Funktion zurückgibt. Der einzige Unterschied besteht darin, dass im generierten asynchronen Zustandsautomaten das Builder-Feld zum angegebenen AsyncFuncAwaitableMethodBuilder anstelle des AsyncTaskMethodBuilder für die Aufgabe wird. Und anscheinend kann diese asynchrone Funktion im Erwartungsausdruck aufgerufen werden, da sie den erwartebaren Typ zurückgibt:

internal static async Task CallReturnFuncAwaitable<T>(T value)
{
    T result = await ReturnFuncAwaitable(value);
}

ValueTask und Leistung

Mit der allgemeinen Unterstützung des asynchronen Rückgabetyps stellt Microsoft auch eine System.Threading.Tasks.ValueTask-erwartete Struktur im NuGet-Paket System.Threading.Tasks.Extensions bereit:

namespace System.Threading.Tasks
{
    [AsyncMethodBuilder(typeof(AsyncValueTaskMethodBuilder<>))]
    [StructLayout(LayoutKind.Auto)]
    public struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>
    {
        public ValueTask(TResult result);

        public ValueTask(Task<TResult> task);

        public ValueTaskAwaiter<TResult> GetAwaiter();

        // Other members.
    }
}

Sein Awaiter ist System.Threading.Tasks.ValueTaskAwaiter, und sein Generator für asynchrone Methoden ist als System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder angegeben, die im selben Paket bereitgestellt werden. Als Werttyp ist ValueTask billiger zuzuweisen als der Referenztyp Task. Im Gegensatz zu Task als Wrapper des Vorgangs Func kann ValueTask ein Wrapper entweder des Vorgangs Func oder des bereits verfügbaren TResult-Ergebnisses sein. ValueTask kann also die Leistung für asynchrone Funktionen verbessern, die möglicherweise über ein Ergebnis verfügen, bevor auf einen asynchronen Vorgang gewartet wird. Das folgende Beispiel lädt Daten vom angegebenen URI herunter:

private static Dictionary<string, byte[]> cache = 
    new Dictionary<string, byte[]>(StringComparer.OrdinalIgnoreCase);

internal static async Task<byte[]> DownloadAsyncTask(string uri)
{
    if (cache.TryGetValue(uri, out byte[] cachedResult))
    {
        return cachedResult;
    }
    using (HttpClient httpClient = new HttpClient())
    {
        byte[] result = await httpClient.GetByteArrayAsync(uri);
        cache.Add(uri, result);
        return result;
    }
}

Es überprüft zuerst den Cache, ob die Daten bereits für den angegebenen URI zwischengespeichert sind, und gibt dann die zwischengespeicherten Daten zurück, ohne eine asynchrone Operation auszuführen. Da die Funktion jedoch über den async-Modifizierer verfügt, wird der gesamte Workflow zur Kompilierzeit zu einem asynchronen Zustandsautomaten. Zur Laufzeit wird eine Aufgabe immer im verwalteten Heap zugewiesen und sollte der Garbage Collection unterzogen werden, und die asynchrone Zustandsmaschine wird immer ausgeführt, selbst wenn das Ergebnis im Cache verfügbar ist und kein asynchroner Vorgang erforderlich ist. Mit ValueTask lässt sich das ganz einfach optimieren:

internal static ValueTask<byte[]> DownloadAsyncValueTask(string uri)
{
    return cache.TryGetValue(uri, out byte[] cachedResult)
        ? new ValueTask<byte[]>(cachedResult)
        : new ValueTask<byte[]>(DownloadAsync());

    async Task<byte[]> DownloadAsync()
    {
        using (HttpClient httpClient = new HttpClient())
        {
            byte[] result = await httpClient.GetByteArrayAsync(uri);
            cache.Add(uri, result);
            return result;
        }
    }
}

Jetzt wird die Funktion zu einer Synchronisierungsfunktion, die ValueTask zurückgibt, auf die gewartet werden kann. Wenn das Ergebnis im Cache verfügbar ist, ist keine asynchrone Operation oder asynchrone Zustandsmaschine beteiligt, und im verwalteten Heap ist keine Aufgabe zugewiesen. Die asynchrone Operation ist in der asynchronen lokalen Funktion gekapselt, die in eine asynchrone Zustandsmaschine kompiliert wird, und ist nur beteiligt, wenn das Ergebnis nicht im Cache verfügbar ist. Dadurch kann die Leistung verbessert werden, insbesondere wenn der Cache häufig getroffen wird. In der Praxis vergleichen Sie bitte die Leistung, um zu entscheiden, welches Muster verwendet werden soll.

Anonyme asynchrone Funktion

Die Schlüsselwörter async und await können mit dem Lambda-Ausdruck verwendet werden:

internal static async Task AsyncLambda(string readPath, string writePath)
{
    Func<string, Task<string>> readAsync = async (path) =>
    {
        using (StreamReader reader = new StreamReader(new FileStream(
            path: path, mode: FileMode.Open, access: FileAccess.Read,
            share: FileShare.Read, bufferSize: 4096, useAsync: true)))
        {
            return await reader.ReadToEndAsync();
        }
    };
    Func<string, string, Task> writeAsync = async (path, contents) =>
    {
        using (StreamWriter writer = new StreamWriter(new FileStream(
            path: path, mode: FileMode.Create, access: FileAccess.Write,
            share: FileShare.Read, bufferSize: 4096, useAsync: true)))
        {
            await writer.WriteAsync(contents);
        }
    };

    string result = await readAsync(readPath);
    await writeAsync(writePath, result); 
}

Hier werden diese 2 asynchronen Lambda-Ausdrücke als Anzeigeklassenmethoden kompiliert, im gleichen Muster wie normale Sync-Lambda-Ausdrücke.

Da eine Aufgabe mit einer anonymen Funktion erstellt werden kann, die einen beliebigen Typ zurückgibt, kann sie auch mit einer asynchronen anonymen Funktion erstellt werden, die eine Aufgabe zurückgibt:

internal static async Task AsyncAnonymous(string readPath, string writePath)
{
    Task<Task<string>> task1 = new Task<Task<string>>(async () => await ReadAsync(readPath));
    task1.Start(); // Cold task needs to be started.
    string contents = await task1.Unwrap(); // Equivalent to: string contents = await await task1;

    Task<Task> task2 = new Task<Task>(async () => await WriteAsync(writePath, null));
    task2.Start(); // Cold task needs to be started.
    await task2.Unwrap(); // Equivalent to: await await task2;
}

Die erste Aufgabe wird mit einer asynchronen anonymen Funktion vom Typ () –> Task erstellt, sodass die erstellte Aufgabe vom Typ Task> ist. In ähnlicher Weise wird die zweite Aufgabe mit einer asynchronen anonymen Funktion vom Typ () –> Task erstellt, sodass die erstellte Aufgabe vom Typ Task ist. Wie bereits erwähnt, können verschachtelte Tasks ausgepackt und abgewartet werden. Für dieses Szenario werden Überladungen von Task.Run bereitgestellt, um asynchrone Funktionen zu akzeptieren und die verschachtelte Aufgabe automatisch auszupacken:

namespace System.Threading.Tasks
{
    public partial class Task : IAsyncResult
    {
        public static Task Run(Func<Task> function);

        public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
    }
}

Das obige Beispiel kann nun vereinfacht werden als:

internal static async Task RunAsync(string readPath, string writePath)
{
    Task<string> task1 = Task.Run(async () => await ReadAsync(readPath)); // Automatically unwrapped.
    string contents = await task1; // Task.Run returns hot task..

    Task task2 = Task.Run(async () => await WriteAsync(writePath, contents)); // Automatically unwrapped.
    await task2; // Task.Run returns hot task.
}