Implementera det aktivitetsbaserade asynkrona mönstret
Du kan implementera det aktivitetsbaserade Asynkrona mönstret (TAP) på tre sätt: genom att använda C# och Visual Basic-kompilatorerna i Visual Studio manuellt eller genom en kombination av kompilatorn och manuella metoder. I följande avsnitt beskrivs varje metod i detalj. Du kan använda TAP-mönstret för att implementera både beräkningsbundna och I/O-bundna asynkrona åtgärder. I avsnittet Arbetsbelastningar beskrivs varje typ av åtgärd.
Generera TAP-metoder
Använda kompilatorerna
Från och med .NET Framework 4.5 betraktas alla metoder som tillskrivs nyckelordet async
(Async
i Visual Basic) som en asynkron metod, och C# och Visual Basic-kompilatorerna utför nödvändiga transformeringar för att implementera metoden asynkront med hjälp av TAP. En asynkron metod ska returnera antingen ett System.Threading.Tasks.Task objekt eller ett System.Threading.Tasks.Task<TResult> objekt. För det senare ska funktionens brödtext returnera en TResult
, och kompilatorn ser till att det här resultatet görs tillgängligt via det resulterande aktivitetsobjektet. På samma sätt kopplas alla undantag som inte hanteras i metodens brödtext till utdataaktiviteten och gör att den resulterande aktiviteten slutar i TaskStatus.Faulted tillståndet. Undantaget till den här regeln är när en OperationCanceledException (eller härledd typ) går ohanterad, i vilket fall den resulterande aktiviteten slutar i tillståndet TaskStatus.Canceled .
Generera TAP-metoder manuellt
Du kan implementera TAP-mönstret manuellt för bättre kontroll över implementeringen. Kompilatorn förlitar sig på den offentliga ytan som exponeras från System.Threading.Tasks namnområdet och stödtyperna System.Runtime.CompilerServices i namnområdet. Om du vill implementera TAP själv skapar du ett TaskCompletionSource<TResult> objekt, utför den asynkrona åtgärden och när den är klar anropar SetResultdu metoden , SetExceptioneller SetCanceled eller Try
versionen av någon av dessa metoder. När du implementerar en TAP-metod manuellt måste du slutföra den resulterande uppgiften när den representerade asynkrona åtgärden slutförs. Till exempel:
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
<Extension()>
Public Function ReadTask(stream As Stream, buffer() As Byte,
offset As Integer, count As Integer,
state As Object) As Task(Of Integer)
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count, Sub(ar)
Try
tcs.SetResult(stream.EndRead(ar))
Catch exc As Exception
tcs.SetException(exc)
End Try
End Sub, state)
Return tcs.Task
End Function
Hybridmetod
Det kan vara användbart att implementera TAP-mönstret manuellt, men att delegera kärnlogik för implementeringen till kompilatorn. Du kanske till exempel vill använda hybridmetoden när du vill verifiera argument utanför en kompilatorgenererad asynkron metod så att undantag kan komma från metodens direktanropare i stället för att exponeras via System.Threading.Tasks.Task objektet:
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
If input Is Nothing Then Throw New ArgumentNullException("input")
Return MethodAsyncInternal(input)
End Function
Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
' code that uses await goes here
return value
End Function
Ett annat fall där en sådan delegering är användbar är när du implementerar snabbsökvägsoptimering och vill returnera en cachelagrad uppgift.
Arbetsbelastningar
Du kan implementera både beräkningsbundna och I/O-bundna asynkrona åtgärder som TAP-metoder. Men när TAP-metoder exponeras offentligt från ett bibliotek bör de endast tillhandahållas för arbetsbelastningar som omfattar I/O-bundna åtgärder (de kan också omfatta beräkning, men bör inte vara rent beräkningsmässiga). Om en metod är rent beräkningsbunden ska den endast exponeras som en synkron implementering. Koden som använder den kan sedan välja om du vill omsluta en anrop av den synkrona metoden till en uppgift för att avlasta arbetet till en annan tråd eller för att uppnå parallellitet. Och om en metod är I/O-bunden ska den endast exponeras som en asynkron implementering.
Beräkningsbundna uppgifter
Klassen System.Threading.Tasks.Task är idealisk för att representera beräkningsintensiva åtgärder. Som standard drar den nytta av särskilt stöd i ThreadPool klassen för att tillhandahålla effektiv körning, och det ger också betydande kontroll över när, var och hur asynkrona beräkningar körs.
Du kan generera beräkningsbundna uppgifter på följande sätt:
I .NET Framework 4.5 och senare versioner (inklusive .NET Core och .NET 5+) använder du den statiska Task.Run metoden som en genväg till TaskFactory.StartNew. Du kan använda Run för att enkelt starta en beräkningsbunden uppgift som riktar sig mot trådpoolen. Det här är den bästa mekanismen för att starta en beräkningsbunden uppgift. Använd
StartNew
bara direkt när du vill ha mer detaljerad kontroll över uppgiften.I .NET Framework 4 använder du TaskFactory.StartNew metoden som accepterar att ett ombud (vanligtvis en Action<T> eller ett Func<TResult>) körs asynkront. Om du anger ett Action<T> ombud returnerar metoden ett System.Threading.Tasks.Task objekt som representerar den asynkrona körningen av ombudet. Om du anger ett Func<TResult> ombud returnerar metoden ett System.Threading.Tasks.Task<TResult> objekt. Överlagringar av StartNew metoden accepterar en annulleringstoken (CancellationToken), alternativ för att skapa aktiviteter (TaskCreationOptions) och en schemaläggare (TaskScheduler), som alla ger detaljerad kontroll över schemaläggningen och körningen av aktiviteten. En fabriksinstans som riktar sig till den aktuella schemaläggaren är tillgänglig som en statisk egenskap (Factory) för Task klassen, till exempel:
Task.Factory.StartNew(…)
.Använd konstruktorerna av
Task
typen ochStart
metoden om du vill generera och schemalägga aktiviteten separat. Offentliga metoder får endast returnera uppgifter som redan har startats.Använd metodens överlagringar Task.ContinueWith . Den här metoden skapar en ny aktivitet som schemaläggs när en annan aktivitet slutförs. Vissa överlagringar ContinueWith accepterar en annulleringstoken, fortsättningsalternativ och en schemaläggare för bättre kontroll över schemaläggningen och körningen av fortsättningsaktiviteten.
TaskFactory.ContinueWhenAll Använd metoderna ochTaskFactory.ContinueWhenAny. Dessa metoder skapar en ny aktivitet som schemaläggs när alla eller någon av en angiven uppsättning aktiviteter slutförs. Dessa metoder ger också överlagringar för att styra schemaläggningen och körningen av dessa uppgifter.
I beräkningsbundna uppgifter kan systemet förhindra körning av en schemalagd aktivitet om den tar emot en begäran om annullering innan den börjar köra aktiviteten. Om du anger en annulleringstoken (CancellationToken -objekt) kan du därför skicka den token till den asynkrona kod som övervakar token. Du kan också ange token till någon av de tidigare nämnda metoderna, till exempel StartNew
eller Run
så att körningen Task
också kan övervaka token.
Tänk dig till exempel en asynkron metod som återger en bild. Uppgiftens brödtext kan avsöka annulleringstoken så att koden kan avslutas tidigt om en begäran om annullering tas emot under återgivningen. Om annulleringsbegäran kommer innan renderingen startar vill du dessutom förhindra återgivningsåtgärden:
internal Task<Bitmap> RenderAsync(
ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for(int y=0; y<data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for(int x=0; x<data.Width; x++)
{
// render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As _
CancellationToken) As Task(Of Bitmap)
Return Task.Run(Function()
Dim bmp As New Bitmap(data.Width, data.Height)
For y As Integer = 0 to data.Height - 1
cancellationToken.ThrowIfCancellationRequested()
For x As Integer = 0 To data.Width - 1
' render pixel [x,y] into bmp
Next
Next
Return bmp
End Function, cancellationToken)
End Function
Beräkningsbundna uppgifter slutar i ett Canceled tillstånd om minst ett av följande villkor är sant:
En begäran om annullering skickas via CancellationToken objektet, som tillhandahålls som ett argument till skapandemetoden (till exempel
StartNew
ellerRun
) innan aktiviteten övergår till tillståndet Running .Ett OperationCanceledException undantag hanteras inte i brödtexten för en sådan aktivitet, undantaget innehåller samma CancellationToken som skickas till aktiviteten och den token visar att annullering begärs.
Om ett annat undantag inte hanteras i aktivitetens brödtext slutar aktiviteten i Faulted tillståndet och eventuella försök att vänta på aktiviteten eller komma åt resultatet gör att ett undantag utlöses.
I/O-bundna uppgifter
Om du vill skapa en uppgift som inte ska säkerhetskopieras direkt av en tråd för hela körningen använder du typen TaskCompletionSource<TResult> . Den här typen exponerar en Task egenskap som returnerar en associerad Task<TResult> instans. Livscykeln för den här uppgiften styrs av TaskCompletionSource<TResult> metoder som SetResult, SetException, SetCanceledoch deras TrySet
varianter.
Anta att du vill skapa en uppgift som ska slutföras efter en angiven tidsperiod. Du kanske till exempel vill fördröja en aktivitet i användargränssnittet. Klassen System.Threading.Timer ger redan möjlighet att asynkront anropa ett ombud efter en angiven tidsperiod, och genom att använda TaskCompletionSource<TResult> kan du placera en Task<TResult> front på timern, till exempel:
public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
TaskCompletionSource<DateTimeOffset> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(DateTimeOffset.UtcNow);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<DateTimeOffset>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
Dim timer As Timer = Nothing
timer = New Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(DateTimeOffset.UtcNow)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
Metoden Task.Delay tillhandahålls för detta ändamål och du kan använda den i en annan asynkron metod, till exempel för att implementera en asynkron avsökningsloop:
public static async Task Poll(Uri url, CancellationToken cancellationToken,
IProgress<bool> progress)
{
while(true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
progress As IProgress(Of Boolean)) As Task
Do While True
Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
Dim success As Boolean = False
Try
await DownloadStringAsync(url)
success = true
Catch
' ignore errors
End Try
progress.Report(success)
Loop
End Function
Klassen TaskCompletionSource<TResult> har ingen icke-generisk motsvarighet. Härleds dock Task<TResult> från Task, så att du kan använda det generiska TaskCompletionSource<TResult> objektet för I/O-bundna metoder som helt enkelt returnerar en uppgift. För att göra detta kan du använda en källa med en dummy TResult
(Boolean är ett bra standardval, men om du är orolig för att användaren av Task nedarbetar den till en Task<TResult>kan du använda en privat TResult
typ i stället). Metoden i föregående exempel returnerar till exempel Delay
den aktuella tiden tillsammans med den resulterande förskjutningen (Task<DateTimeOffset>
). Om ett sådant resultatvärde inte behövs kan metoden i stället kodas enligt följande (observera ändringen av returtyp och ändringen av argumentet till TrySetResult):
public static Task<bool> Delay(int millisecondsTimeout)
{
TaskCompletionSource<bool> tcs = null;
Timer timer = null;
timer = new Timer(delegate
{
timer.Dispose();
tcs.TrySetResult(true);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<bool>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of Boolean)
Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
Dim timer As Timer = Nothing
Timer = new Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(True)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of Boolean)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
Blandade beräkningsbundna och I/O-bundna uppgifter
Asynkrona metoder är inte begränsade till enbart beräkningsbundna eller I/O-bundna åtgärder, utan kan representera en blandning av de två. Faktum är att flera asynkrona åtgärder ofta kombineras till större blandade åtgärder. Metoden i ett tidigare exempel utförde till exempel RenderAsync
en beräkningsintensiv åtgärd för att återge en bild baserat på vissa indata imageData
. Detta imageData
kan komma från en webbtjänst som du asynkront kommer åt:
public async Task<Bitmap> DownloadDataAndRenderImageAsync(
CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(
cancellationToken As CancellationToken) As Task(Of Bitmap)
Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
Return Await RenderAsync(imageData, cancellationToken)
End Function
Det här exemplet visar också hur en enskild annulleringstoken kan trådas genom flera asynkrona åtgärder. Mer information finns i avsnittet om avbokningsanvändning i Använda det aktivitetsbaserade asynkrona mönstret.