Approfondimenti sulla programmazione funzionale in C# (14) Funzione asincrona

Approfondimenti sulla programmazione funzionale in C# (14) Funzione asincrona

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione:https://weblogs.asp.net/dixin/function-csharp-asynchronous-function

La funzione asincrona può migliorare la reattività e la scalabilità dell'applicazione e del servizio. C# 5.0 introduce parole chiave async e await per semplificare notevolmente il modello di programmazione asincrono.

Attività, attività e asincronia

Nel modello di programmazione asincrona C#/.NET, System.Threading.Tasks.Task viene fornito per rappresentare l'operazione asincrona che restituisce void e System.Threading.Tasks.Task viene fornito per rappresentare l'operazione asincrona che restituisce il valore TResult:

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 e Task possono essere costruiti con () –> funzione void e () –> TResult e possono essere avviati chiamando il metodo Start. Un'attività viene eseguita in modo asincrono e non blocca il thread corrente. Il suo stato può essere richiesto dalle proprietà Status, IsCanceled, IsCompleted, IsFaulted. Un'attività può essere attesa chiamando il relativo metodo Wait, che blocca il thread corrente finché l'attività non viene completata correttamente, non riesce o viene annullata. Per Task, quando l'operazione di sincronizzazione sottostante viene completata correttamente, il risultato è disponibile tramite la proprietà Result. Per Task o Task, l'operazione asincrona sottostante non riesce con un'eccezione, l'eccezione è disponibile tramite la proprietà Exception. Un'attività può essere concatenata con un'altra operazione di continuazione asincrona chiamando i metodi ContinueWith. Al termine dell'esecuzione dell'attività, la continuazione specificata inizia a essere eseguita in modo asincrono. Se l'attività termina già l'esecuzione quando viene chiamato il relativo metodo ContinueWith, la continuazione specificata viene avviata immediatamente. L'esempio seguente costruisce e avvia un'attività per leggere un file e concatena un'altra attività di continuazione per scrivere il contenuto in un altro file:

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

Come operazioni asincrone, quando le attività vengono avviate, le funzioni avvolte sono pianificate per impostazione predefinita per l'esecuzione nel pool di thread CLR/CoreCLR, in modo che i loro ID thread siano diversi dall'ID thread chiamante.

Task fornisce anche i metodi Run per creare e avviare automaticamente attività:

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

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

Ora confronta le seguenti funzioni:

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

Quando viene chiamato Write, la sua esecuzione blocca il thread corrente. Quando l'operazione di scrittura viene eseguita in modo sincrono, restituisce senza risultato e quindi il thread chiamante può continuare l'esecuzione. Allo stesso modo, quando viene chiamato Read, la sua esecuzione blocca anche il thread corrente. Quando l'operazione di lettura viene eseguita in modo sincrono, restituisce il risultato, in modo che il risultato sia disponibile per il chiamante e il chiamante possa continuare l'esecuzione. Quando viene chiamato WriteAsync, chiama Task.Run per costruire un'istanza di Task con l'operazione di scrittura, avvia l'attività, quindi restituisce immediatamente l'attività. Quindi il chiamante può continuare senza essere bloccato dall'esecuzione dell'operazione di scrittura. Per impostazione predefinita, l'operazione di scrittura è pianificata nel pool di thread, al termine, l'operazione di scrittura non restituisce alcun risultato e lo stato dell'attività viene aggiornato. Allo stesso modo, quando viene chiamato ReadAsync, chiama anche Task.Run per costruire un'istanza Task con l'operazione di lettura, avviare l'attività, quindi restituire immediatamente l'attività. Quindi il chiamante può continuare senza essere bloccato dall'esecuzione dell'operazione di lettura. Per impostazione predefinita, l'operazione di lettura è pianificata anche nel pool di thread, quando viene eseguita, l'operazione di lettura ha un risultato e lo stato dell'attività viene aggiornato, con il risultato disponibile tramite la proprietà Result.

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

Quindi Write che restituisce void e Read che restituisce un risultato sono funzioni di sincronizzazione. WriteAsync che restituisce Task e ReadAsync che restituisce Task sono funzioni asincrone, in cui Task può essere visualizzato come vuoto futuro e Task può essere visualizzato come risultato TResult futuro. Qui WriteAsync e ReadAsync diventano asincroni semplicemente scaricando le operazioni nel pool di thread. Questo è a scopo dimostrativo e non apporta alcun miglioramento della scalabilità. Una migliore implementazione verrà discussa in seguito.

Funzione asincrona denominata

Per impostazione predefinita, la funzione asincrona denominata restituisce Task o Task e ha un suffisso Async o AsyncTask nel nome come convenzione. L'esempio seguente è un flusso di lavoro di lettura e scrittura di file di chiamate alla funzione di sincronizzazione:

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

La stessa logica può essere implementata chiamando la versione asincrona delle funzioni:

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

Qui await viene usato per ogni chiamata di funzione asincrona e la struttura del codice rimane la stessa del flusso di lavoro di sincronizzazione. Quando la parola chiave await viene utilizzata nel corpo della funzione, per quella funzione è richiesto il modificatore async. Per quanto riguarda il flusso di lavoro non restituisce alcun risultato, la funzione asincrona restituisce Task (future void). Questa funzione ReadWriteAsync chiama funzioni asincrone, essa stessa è anche funzione asincrona, poiché ha il modificatore asincrono e restituisce Task. Quando viene chiamato ReadWriteAsync, funziona allo stesso modo di ReadAsync e WriteAsync. non blocca il chiamante e restituisce immediatamente un'attività per rappresentare il flusso di lavoro di lettura e scrittura pianificato.

Pertanto, la parola chiave await può essere vista come un'attesa virtuale del completamento dell'operazione di sincronizzazione asincrona dell'attività. Se l'attività non riesce, viene generata un'eccezione. Se l'attività viene completata correttamente, viene richiamata la continuazione subito dopo l'espressione await. Se l'attività ha un risultato, await può estrarre il risultato. Pertanto, il flusso di lavoro asincrono mantiene lo stesso aspetto del flusso di lavoro di sincronizzazione. Non è necessaria alcuna chiamata ContinueWith per creare la continuazione. L'esempio seguente è un flusso di lavoro di query del database più complesso di chiamate alla funzione di sincronizzazione e viene restituito un valore int come risultato della query:

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

Qui i metodi DbConnection.Open, DbCommand.ExecuteReader, DbDataReader.Read, StreamWriter.WriteLine hanno una versione asincrona fornita come DbConnection.OpenAsync, DbCommand.ExecuteReaderAsync, DbDataReader.ReadAsync, StreamWriter.WriteLineAsync. Restituiscono Task o Task. Con le parole chiave async e await, è facile chiamare queste funzioni asincrone:

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

Anche in questo caso, il flusso di lavoro asincrono mantiene la stessa struttura di codice del flusso di lavoro di sincronizzazione, il try-catch, utilizzando, se il blocco ha lo stesso aspetto. Senza questa sintassi, è molto più complesso chiamare ContinueWith e creare manualmente sopra il flusso di lavoro. Per quanto riguarda la funzione asincrona restituisce un risultato int, il suo tipo restituito è Task (future int).

Le funzioni di scrittura e lettura precedenti chiamano File.WriteAllText e File.ReadAllText per eseguire operazioni di I/O di sincronizzazione, che vengono implementate internamente chiamando StreamWriter.Write e StreamReader.ReadToEnd. Ora con le parole chiave async e await, WriteAsync e ReadAsync possono essere implementati come I/O asincroni reali (a condizione che il sistema operativo sottostante supporti I/O asincroni) chiamando StreamWriter.WriteAsync e StreamReader.ReadToEndAsync:

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

Esiste uno scenario speciale in cui la funzione asincrona deve restituire void invece di Task:il gestore di eventi asincrono. Ad esempio, ObservableCollection ha un evento CollectionChanged:

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

Questo evento richiede che il relativo gestore sia una funzione di tipo (oggetto, NotifyCollectionChangedEventArgs) –> void. Quindi, quando si definisce una funzione asincrona come gestore dell'evento sopra, quella funzione asincrona deve restituire void invece di Task:

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

Oltre all'attività restituita dalle funzioni asincrone, la parola chiave await funziona con qualsiasi istanza Task e Task:

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

Se un'attività non viene mai avviata, non finisce mai di essere eseguita. Il codice dopo la sua espressione await non viene mai richiamato:

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

L'attività non ancora avviata è chiamata attività a freddo e l'attività già avviata è chiamata attività a caldo. Per convenzione, qualsiasi funzione che restituisce un'attività dovrebbe sempre restituire un'attività attiva. Tutte le API .NET seguono questa convenzione.

Modello Awaitable-waiter

C# compila l'espressione await con il modello awaitable-awaiter. Oltre a Task e Task, la parola chiave await può essere usata con qualsiasi tipo awaitable. Un tipo awaitable ha un'istanza GetAwaiter o un metodo di estensione per restituire un awaiter. Un tipo awaiter implementa l'interfaccia System.Runtime.CompilerServices.INotifyCompletion, ha anche una proprietà IsCompleted che restituisce un valore bool e un metodo di istanza GetResult che restituisce void o un valore di risultato. Le seguenti interfacce IAwaitable e IAwaiter mostrano il modello awaitable-awaiter per le operazioni senza risultato:

public interface IAwaitable
{
    IAwaiter GetAwaiter();
}

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

    void GetResult(); // No result.
}

E le seguenti interfacce IAwaitable e IAwaiter mostrano il modello awaitable-awaiter per le operazioni con un risultato:

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

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

    TResult GetResult(); // TResult result.
}

E l'interfaccia INotifyCompletion ha un unico metodo OnCompleted per concatenare una continuazione:

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

Ecco come Task e Task implementano il modello awaitable-awaiter. L'attività può essere vista virtualmente come implementazione di IAwaitable, ha un metodo di istanza GetAwaiter che restituisce System.Runtime.CompilerServices.TaskAwaiter, che può essere virtualmente visto come implementazione di IAwaiter; Allo stesso modo, Task può essere virtualmente visto come implementazione di IAwaitable, ha un metodo GetAwaiter che restituisce System.Runtime.CompilerServices.TaskAwaiter, che può essere virtualmente visto come implementazione di IAwaiter:

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

Qualsiasi altro tipo può essere utilizzato con la parola chiave await, purché sia ​​implementato il modello awaitable-awaiter. Prendi l'azione come esempio, un metodo GetAwaiter può essere facilmente implementato come metodo di estensione, riutilizzando sopra TaskAwaiter:

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

Allo stesso modo, questo modello può essere implementato per Func, riutilizzando TaskAwaiter:

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

Ora la parola chiave await può essere utilizzata direttamente con una funzione:

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

Macchina a stati asincroni

Come accennato in precedenza, con le parole chiave async e await, una funzione asincrona non è bloccante. In fase di compilazione, il flusso di lavoro di una funzione asincrona viene compilato in una macchina a stati asincrona. In fase di esecuzione, quando viene chiamata questa funzione asincrona, avvia semplicemente la macchina a stati asincrona generata dal compilatore e restituisce immediatamente un'attività che rappresenta il flusso di lavoro nella macchina a stati asincrona. Per dimostrarlo, definisci i seguenti metodi asincroni:

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;

Dopo la compilazione, il modificatore asincrono è scomparso. La funzione asincrona diventa una normale funzione per avviare una macchina a stati asincrona:

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

E la macchina a stati asincrona generata è una struttura nella build di rilascio e una classe nella build di debug:

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

La macchina a stati asincrona generata è una macchina a stati finiti:

Il flusso di lavoro viene compilato nel relativo metodo MoveNext e il flusso di lavoro è suddiviso in 4 blocchi dalle 3 parole chiave await. Il parametro del flusso di lavoro viene compilato come un campo della macchina a stati, in modo che sia accessibile dal flusso di lavoro all'interno di MoveNext. Quando la macchina a stati viene inizializzata, il suo stato iniziale è –1, che significa start. Una volta avviata la macchina a stati, viene chiamato MoveNext e viene eseguito il blocco case –1, che contiene il codice dall'inizio del flusso di lavoro alla prima espressione await, che viene compilata in una chiamata GetAwaiter. Se l'awaiter è già completato, la continuazione deve essere eseguita immediatamente, quindi viene eseguito il blocco case 0 successivo; Se l'awaiter non viene completato, la continuazione (chiamata MoveNext con stato successivo 0) viene specificata come callback dell'awaiter quando verrà completata in futuro. In entrambi i casi, quando viene eseguito il codice in caso di blocco 0, il precedente waiter è già completato e il suo risultato è immediatamente disponibile tramite il metodo GetResult. L'esecuzione continua nello stesso schema, fino a quando non viene eseguito l'ultimo blocco del caso 2.

Acquisizione del contesto di runtime

Per ogni espressione await, se l'attività attesa non è ancora stata completata, la continuazione viene pianificata come callback una volta completata. Di conseguenza, la continuazione può essere eseguita da un thread diverso dal thread chiamante iniziale. Per impostazione predefinita, le informazioni sul contesto di runtime del thread iniziale vengono acquisite e riutilizzate da per eseguire la continuazione. Per dimostrarlo, il modello awaiter-awaiter di cui sopra per Action può essere implementato nuovamente con un awaiter personalizzato:

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

Quando l'awaiter viene creato, acquisisce le informazioni sul contesto di runtime, inclusi System.Threading.SynchronizationContext, System.Threading.Tasks.TaskScheduler e System.Threading.ExecutionContext del thread corrente. Quindi in OnCompleted, quando la continuazione viene richiamata, viene eseguita con le informazioni sul contesto di runtime acquisite in precedenza. L'awaiter personalizzato può essere implementato per Func nello stesso schema:

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

Quella che segue è un'implementazione di base dell'acquisizione e ripristino del contesto di runtime:

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

Quando viene eseguita la continuazione, viene prima verificato il SynchronizationContext precedentemente acquisito. Se viene acquisito un SynchronizationContext specializzato ed è diverso dal SynchronizationContext corrente, la continuazione viene eseguita con SynchronizationContext ed ExecutionContext acquisiti. Quando non è stato acquisito alcun SynchronizationContext specializzato, viene controllato il TaskScheduler. Se viene acquisito un TaskScheduler specializzato, viene utilizzato per pianificare la continuazione come attività. Per tutti gli altri casi, la continuazione viene eseguita con l'ExecutionContext acquisito.

Task e Task forniscono un metodo ConfigureAwait per specificare se il marshalling della continuazione viene eseguito nel contesto di runtime acquisito in precedenza:

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

Per dimostrare l'acquisizione del contesto di runtime, definisci un'utilità di pianificazione personalizzata, che avvia semplicemente un thread in background per eseguire ciascuna attività:

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

La seguente funzione asincrona ha 2 espressioni await, in cui ConfigureAwait viene chiamato con valori bool diversi:

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
}

Per dimostrare l'acquisizione dell'utilità di pianificazione delle attività, chiama la funzione asincrona sopra specificando l'utilità di pianificazione delle attività personalizzata:

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

In questo caso, poiché la funzione asincrona ConfigureRuntimeContextCapture restituisce Task, quindi l'attività costruita con la funzione asincrona è di tipo Task. Viene fornito un metodo di estensione Unwrap per Task per convertirlo in normale Task:

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

Quando viene eseguita la funzione asincrona ConfigureRuntimeContextCapture, l'utilità di pianificazione attività iniziale è l'utilità di pianificazione attività personalizzata specificata. Nella prima espressione await, ConfigureAwait viene chiamato con true, in modo che le informazioni sul contesto di runtime vengano acquisite e la continuazione venga eseguita con le informazioni sul contesto di runtime acquisite. Questo è il comportamento predefinito, quindi chiamare ConfigureAwait con true equivale a non chiamare affatto ConfigureAwait. Di conseguenza, la prima continuazione viene eseguita con lo stesso Utilità di pianificazione personalizzata. Nella seconda espressione await, ConfigureAwait viene chiamato con false, quindi le informazioni sul contesto di runtime non vengono acquisite. Di conseguenza, la seconda continuazione viene eseguita con l'utilità di pianificazione predefinita (System.Threading.Tasks.ThreadPoolTaskScheduler).

L'acquisizione del contesto di runtime può essere dimostrata anche da SynchronizationContext. SynchronizationContext ha diverse implementazioni in diversi modelli di applicazione, ad esempio:

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

Prendi come esempio l'applicazione Windows Universal. In Visual Studio, crea un'applicazione Windows Universal, aggiungi un pulsante alla relativa interfaccia utente:

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

Nel codice sottostante, implementa il gestore dell'evento Click come funzione asincrona:

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

WinRTSynchronizationContext è disponibile solo per il thread dell'interfaccia utente. Quando si fa clic sul pulsante, il thread dell'interfaccia utente esegue la funzione asincrona ButtonClick, quindi il SynchronizationContext iniziale è WinRTSynchronizationContext. Analogamente all'esempio precedente, quando ConfigureAwait viene chiamato con true, la continuazione viene eseguita con il WinRTSynchronizationContext acquisito in precedenza, in modo che la continuazione possa aggiornare correttamente l'interfaccia utente. Quando ConfigureAwait viene chiamato con true, la continuazione non viene eseguita con WinRTSynchronizationContext e non riesce ad aggiornare l'interfaccia utente e genera un'eccezione.

Tipo restituito asincrono generalizzato e generatore di metodi asincroni

Da C# 7, la funzione asincrona è supportata per restituire qualsiasi tipo awaitable, purché abbia un generatore di metodi asincrono specificato. Ad esempio, il seguente FuncAwaitable è un tipo awaitable, riutilizza sopra FuncAwater come awaiter:

[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 è già disponibile con il metodo di estensione GetAwaiter sopra, ma qui viene implementato un tipo di wrapper di questo tipo, in modo che sia possibile specificare un generatore di metodi asincrono, con un attributo [AsyncMethodBuilder]. Il generatore di metodi asincroni è definito come:

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

Ora il tipo FuncAwitable può essere restituito dalla funzione asincrona:

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

La sua compilazione segue lo stesso schema della funzione asincrona che restituisce l'attività. L'unica differenza è che, nella macchina a stati asincrona generata, il campo del builder diventa AsyncFuncAwaitableMethodBuilder specificato, invece di AsyncTaskMethodBuilder per l'attività. E a quanto pare, questa funzione asincrona può essere chiamata nell'espressione await poiché restituisce un tipo awaitable:

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

ValueTask e prestazioni

Con il supporto del tipo restituito asincrono generalizzato, Microsoft fornisce anche una struttura disponibile System.Threading.Tasks.ValueTask nel pacchetto NuGet System.Threading.Tasks.Extensions:

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

Il relativo awaiter è System.Threading.Tasks.ValueTaskAwaiter e il relativo generatore di metodi asincroni è specificato come System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder, forniti nello stesso pacchetto. Come tipo di valore, ValueTask è più economico da allocare rispetto al tipo di riferimento Task. Inoltre, a differenza di Task come wrapper dell'operazione Func, ValueTask può essere un wrapper dell'operazione Func o del risultato TResult che è già disponibile. Quindi ValueTask può migliorare le prestazioni per la funzione asincrona che potrebbe avere risultati disponibili prima di attendere qualsiasi operazione asincrona. L'esempio seguente scarica i dati dall'URI specificato:

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

Prima controlla la cache, se i dati sono già memorizzati nella cache per l'URI specificato, quindi restituisce i dati memorizzati nella cache senza eseguire alcuna operazione asincrona. Tuttavia, in fase di compilazione, poiché la funzione ha il modificatore async, l'intero flusso di lavoro diventa una macchina a stati asincrona. In fase di esecuzione, un'attività viene sempre allocata nell'heap gestito e deve essere sottoposta a Garbage Collection e la macchina a stati asincrona viene sempre eseguita, anche quando il risultato è disponibile nella cache e non è necessaria alcuna operazione asincrona. Con ValueTask, questo può essere facilmente ottimizzato:

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

Ora la funzione diventa una funzione di sincronizzazione che restituisce ValueTask, che è in attesa. Quando il risultato è disponibile nella cache, non è coinvolta alcuna operazione asincrona o macchina a stati asincrona e non è presente alcuna attività allocata nell'heap gestito. L'operazione asincrona è incapsulata nella funzione locale asincrona, che viene compilata nella macchina a stati asincrona ed è coinvolta solo quando il risultato non è disponibile nella cache. Di conseguenza, le prestazioni possono essere migliorate, soprattutto quando la cache viene colpita frequentemente. In pratica, confronta le prestazioni per decidere quale modello utilizzare.

Funzione asincrona anonima

Le parole chiave async e await possono essere utilizzate con l'espressione lambda:

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

Qui queste 2 espressioni lambda asincrone vengono compilate come metodi di classe display, nello stesso schema delle normali espressioni lambda di sincronizzazione.

Poiché l'attività può essere costruita con una funzione anonima che restituisce qualsiasi tipo, può essere costruita anche con una funzione anonima asincrona che restituisce un'attività:

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

La prima attività è costruita con una funzione anonima asincrona di tipo () –> Task, quindi l'attività costruita è di tipo Task>. Allo stesso modo, la seconda attività è costruita con una funzione anonima asincrona di tipo () –> Task, quindi l'attività costruita è di tipo Task. Come accennato in precedenza, l'attività nidificata può essere scartata e attesa. Per questo scenario, vengono forniti gli overload di Task.Run per accettare funzioni asincrone e annullare automaticamente l'attività nidificata:

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

L'esempio sopra ora può essere semplificato come:

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