Come implementare await senza Async CTP
-
28-10-2019 - |
Domanda
Come implementereste qualcosa che funzioni in modo simile alla parola chiave await
CTP asincrono?Esiste un'implementazione semplice che funzioni come await
in tutti i casi o await
richiede implementazioni diverse per scenari diversi?
Soluzione
await
implica sempre lo stesso tipo di trasformazione, ma è piuttosto doloroso.Il lato libreria di await
non è troppo complicato, ma la parte complicata è che il compilatore costruisce una macchina a stati per te, consentendo alla continuazione di tornare al posto giusto.
È possibile che il mio uso hacky dei blocchi iteratori (rendimento del rendimento) possa simulare qualcosa di simile ... ma sarebbe piuttosto brutto.
Ho tenuto un webinar DevExpress su ciò che fa il compilatore dietro le quinte qualche settimana fa -che mostra il codice decompilato da un paio di esempi, oltre a spiegare come il compilatore costruisce un'attività da restituire e cosa deve fare il "waititer".Potrebbe esserti utile.
Altri suggerimenti
La nuova parola chiave await
ha una semantica simile alla parola chiave yield return
esistente in quanto entrambe fanno sì che il compilatore generi la macchina a stati dello stile di continuazione per te. Quindi è possibile hackerare qualcosa insieme usando iteratori che hanno alcuni degli stessi comportamenti del CTP Async.
Ecco come sarebbe.
public class Form1 : Form
{
private void Button1_Click(object sender, EventArgs e)
{
AsyncHelper.Invoke<bool>(PerformOperation);
}
private IEnumerable<Task> PerformOperation(TaskCompletionSource<bool> tcs)
{
Button1.Enabled = false;
for (int i = 0; i < 10; i++)
{
textBox1.Text = "Before await " + Thread.CurrentThread.ManagedThreadId.ToString();
yield return SomeOperationAsync(); // Await
textBox1.Text = "After await " + Thread.CurrentThread.ManagedThreadId.ToString();
}
Button2.Enabled = true;
tcs.SetResult(true); // Return true
}
private Task SomeOperationAsync()
{
// Simulate an asynchronous operation.
return Task.Factory.StartNew(() => Thread.Sleep(1000));
}
}
Poiché yield return
genera un IEnumerable
, la nostra coroutine deve restituire un IEnumerable
. Tutta la magia avviene all'interno del metodo AsyncHelper.Invoke
. Questo è ciò che fa funzionare la nostra coroutine (mascherata da iteratore hackerato). Ci vuole particolare cura per assicurarsi che l'iteratore sia sempre eseguito nel contesto di sincronizzazione corrente, se ne esiste uno, il che è importante quando si cerca di simulare il funzionamento di await
su un thread dell'interfaccia utente. Lo fa eseguendo il primo MoveNext
in modo sincrono e quindi utilizzando SynchronizationContext.Send
per fare il resto da un thread di lavoro che viene anche utilizzato per attendere in modo asincrono i singoli passaggi.
public static class AsyncHelper
{
public static Task<T> Invoke<T>(Func<TaskCompletionSource<T>, IEnumerable<Task>> method)
{
var context = SynchronizationContext.Current;
var tcs = new TaskCompletionSource<T>();
var steps = method(tcs);
var enumerator = steps.GetEnumerator();
bool more = enumerator.MoveNext();
Task.Factory.StartNew(
() =>
{
while (more)
{
enumerator.Current.Wait();
if (context != null)
{
context.Send(
state =>
{
more = enumerator.MoveNext();
}
, null);
}
else
{
enumerator.MoveNext();
}
}
}).ContinueWith(
(task) =>
{
if (!tcs.Task.IsCompleted)
{
tcs.SetResult(default(T));
}
});
return tcs.Task;
}
}
L'intera parte sul TaskCompletionSource
era il mio tentativo di replicare il modo in cui await
può "restituire" un valore. Il problema è che la coroutine deve restituire effettivamente un IEnumerable
poiché non è altro che un iteratore hackerato. Quindi avevo bisogno di trovare un meccanismo alternativo per acquisire un valore di ritorno.
Ci sono alcune limitazioni evidenti in questo, ma spero che questo ti dia l'idea generale. Dimostra anche come il CLR potrebbe avere un meccanismo generalizzato per l'implementazione di coroutine per le quali await
e yield return
utilizzerebbero ubiquitariamente, ma in modi diversi per fornire le rispettive semantiche.
Ci sono poche implementazioni ed esempi di coroutine fatte di iteratori (yield).
Uno degli esempi è il framework Caliburn.Micro, che utilizza questo schema per le operazioni GUI asincrone.Ma può essere facilmente generalizzato per il codice asincrono generale.
Il framework MindTouch DReAM implementa Coroutines in cima al pattern Iterator che è funzionalmente molto simile ad Async /Attendi:
async Task Foo() {
await SomeAsyncCall();
}
contro
IYield Result Foo() {
yield return SomeAsyncCall();
}
Result
è la versione DReAM di Task
.Le DLL del framework funzionano con .NET 2.0+, ma per crearle è necessario 3.5, poiché oggigiorno utilizziamo molta sintassi 3.5.
Bill Wagner di Microsoft ha scritto un articolo su MSDN Magazine su comeè possibile utilizzare la libreria Task Parallel in Visual Studio 2010 per implementare un comportamento di tipo asincrono senza aggiungere una dipendenza dal ctp asincrono.
Utilizza Task
e Task<T>
ampiamente, il che ha anche il vantaggio aggiuntivo che una volta uscito C # 5, il tuo codice sarà ben preparato per iniziare a utilizzare async
e await
.
Dalla mia lettura, le principali differenze tra yield return
e await
è che await
può fornire esplicitamente restituire un nuovo valore nella continuazione.
SomeValue someValue = await GetMeSomeValue();
mentre con yield return
, dovresti ottenere la stessa cosa per riferimento.
var asyncOperationHandle = GetMeSomeValueRequest();
yield return asyncOperationHandle;
var someValue = (SomeValue)asyncOperationHandle.Result;