A feladatalapú aszinkron minta implementálása
A feladatalapú aszinkron mintát (TAP) három módon implementálhatja: a C# és a Visual Basic fordítóinak használatával a Visual Studióban manuálisan, vagy a fordító és a manuális metódusok kombinációjával. Az alábbi szakaszok részletesen ismertetik az egyes metódusokat. A TAP-mintával számítási és I/O-kötésű aszinkron műveleteket is implementálhat. A Számítási feladatok szakasz az egyes műveletek típusait ismerteti.
TAP-metódusok létrehozása
A fordítók használata
A .NET-keretrendszer 4.5-től kezdődően a kulcsszóval async
Async
(a Visual Basicben) társított metódusok aszinkron metódusnak minősülnek, a C# és a Visual Basic fordítói pedig elvégzik a szükséges átalakításokat a metódus aszinkron implementálásához a TAP használatával. Az aszinkron metódusnak egy vagy egy System.Threading.Tasks.Task objektumot System.Threading.Tasks.Task<TResult> kell visszaadnia. Az utóbbi esetében a függvény törzsének egy értéket kell visszaadnia TResult
, és a fordító biztosítja, hogy ez az eredmény elérhető legyen az eredményként kapott tevékenységobjektumon keresztül. Hasonlóképpen, a metódus törzsén belül kezeletlen kivételek a kimeneti tevékenységhez lesznek rendezve, és az eredményül kapott tevékenység állapota megszűnik TaskStatus.Faulted . Ez alól a szabály alól kivételt képez, ha egy OperationCanceledException (vagy származtatott) típus kezeletlen lesz, ebben az esetben az eredményül kapott tevékenység az TaskStatus.Canceled állapotban fejeződik be.
TAP-metódusok manuális létrehozása
A TAP-mintát manuálisan is implementálhatja az implementáció jobb szabályozása érdekében. A fordító a névtérből közzétett nyilvános felületre és a System.Threading.TasksSystem.Runtime.CompilerServices névtérben található támogató típusokra támaszkodik. A TAP saját megvalósításához hozzon létre egy TaskCompletionSource<TResult> objektumot, hajtsa végre az aszinkron műveletet, és amikor befejeződik, hívja meg az SetResult, SetExceptionvagy SetCanceled metódust vagy az Try
egyik metódus verzióját. Ha manuálisan implementál egy TAP-metódust, az eredményül kapott feladatot az aszinkron művelet befejeződésekor kell elvégeznie. Példa:
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
Hibrid megközelítés
Hasznos lehet a TAP-minta manuális implementálása, de az implementáció alapvető logikájának delegálása a fordítónak. Érdemes lehet például a hibrid megközelítést használni, ha egy fordító által létrehozott aszinkron metóduson kívül szeretné ellenőrizni az argumentumokat, hogy a kivételek ne az objektumon keresztül System.Threading.Tasks.Task , hanem a metódus közvetlen hívója elől meneküljenek:
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
Egy másik eset, amikor az ilyen delegálás hasznos, ha gyorsútvonal-optimalizálást hajt végre, és gyorsítótárazott feladatot szeretne visszaadni.
Számítási feladatok
A számítási és az I/O-kötésű aszinkron műveleteket TAP-metódusokként is implementálhatja. Ha azonban a TAP-metódusok nyilvánosan elérhetővé válnak egy tárból, csak az I/O-hez kötött műveleteket tartalmazó számítási feladatokhoz kell megadni őket (ezek számítást is magukban foglalhatnak, de nem lehetnek tisztán számítási jellegűek). Ha egy metódus tisztán számítási kötöttségű, akkor csak szinkron implementációként kell elérhetővé tenni. Az azt használó kód ezután döntheti el, hogy a szinkron metódus meghívását egy feladatba csomagolja-e, hogy a munkát egy másik szálra kicsomagolja, vagy párhuzamosságot érjen el. Ha egy metódus I/O-kötésű, akkor csak aszinkron implementációként kell elérhetővé tenni.
Számításhoz kötött tevékenységek
Az System.Threading.Tasks.Task osztály ideálisan alkalmas számítási igényű műveletek ábrázolására. Alapértelmezés szerint kihasználja az osztályon belüli speciális támogatást a ThreadPool hatékony végrehajtás érdekében, és jelentős ellenőrzést biztosít az aszinkron számítások végrehajtásakor, hol és hogyan.
A számításhoz kötött tevékenységeket a következő módokon hozhatja létre:
A .NET-keretrendszer 4.5-ös és újabb verzióiban (beleértve a .NET Core-t és a .NET 5+-ot) használja a statikus Task.Run metódustTaskFactory.StartNew. A szálkészletet célként kitűző számítási feladatok egyszerűen elindíthatók Run . Ez a számítási feladatok indításának előnyben részesített mechanizmusa. Közvetlenül csak akkor használja
StartNew
, ha részletesebben szeretné szabályozni a feladatot.A 4. .NET-keretrendszer használja azt a TaskFactory.StartNew metódust, amely elfogad egy delegáltat (általában egy Action<T> vagy a Func<TResult>) az aszinkron végrehajtáshoz. Ha delegáltat Action<T> ad meg, a metódus egy System.Threading.Tasks.Task olyan objektumot ad vissza, amely a meghatalmazott aszinkron végrehajtását jelöli. Ha meghatalmazottat Func<TResult> ad meg, a metódus egy objektumot System.Threading.Tasks.Task<TResult> ad vissza. A metódus túlterhelései elfogadnak egy lemondási StartNew jogkivonatot (CancellationToken), a tevékenységlétrehozási lehetőségeket (TaskCreationOptions), valamint egy feladatütemezőt (TaskScheduler), amelyek mindegyike részletes vezérlést biztosít a tevékenység ütemezése és végrehajtása felett. Az aktuális feladatütemezőt megcélzó gyári példány az osztály statikus tulajdonságaként Task (Factorypéldául:
Task.Factory.StartNew(…)
.Ha külön szeretné létrehozni és ütemezni a feladatot, használja a
Task
típus és aStart
metódus konstruktorait. A nyilvános metódusoknak csak olyan tevékenységeket szabad visszaadni, amelyek már el lettek indítva.Használja a metódus túlterheléseit Task.ContinueWith . Ez a metódus létrehoz egy új tevékenységet, amely egy másik tevékenység befejezésekor lesz ütemezve. Egyes túlterhelések lemondási ContinueWith jogkivonatot, folytatási lehetőségeket és feladatütemezőt fogadnak el a folytatási tevékenység ütemezésének és végrehajtásának jobb szabályozásához.
Használja a metódusokat és TaskFactory.ContinueWhenAny a TaskFactory.ContinueWhenAll módszereket. Ezek a metódusok létrehoznak egy új feladatot, amely akkor lesz ütemezve, amikor egy megadott tevékenységkészlet vagy bármelyik befejeződött. Ezek a módszerek túlterhelést is biztosítanak ezen tevékenységek ütemezésének és végrehajtásának szabályozásához.
A számítási feladatokban a rendszer megakadályozhatja az ütemezett tevékenységek végrehajtását, ha lemondási kérelmet kap, mielőtt elkezdené futtatni a feladatot. Ilyen esetben, ha lemondási jogkivonatot (CancellationToken objektumot) ad meg, a jogkivonatot átadhatja a jogkivonatot monitorozó aszinkron kódnak. A jogkivonatot a korábban említett módszerek egyikének is megadhatja, például StartNew
Run
úgy, hogy a Task
futtatókörnyezet is figyelhesse a jogkivonatot.
Vegyük például egy kép renderelt aszinkron metódusát. A feladat törzse lekérdezheti a lemondási jogkivonatot, így a kód korán kiléphet, ha egy lemondási kérelem érkezik a renderelés során. Ezenkívül, ha a lemondási kérelem a renderelés megkezdése előtt érkezik, meg kell akadályoznia a renderelési műveletet:
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
A számításhoz kötött tevékenységek állapota akkor fejeződik be Canceled , ha az alábbi feltételek közül legalább az egyik teljesül:
A lemondási kérelem az CancellationToken objektumon keresztül érkezik, amely argumentumként szolgál a létrehozási módszerhez (például
StartNew
Run
) mielőtt a tevékenység áttér az Running állapotra.A OperationCanceledException kivétel nem lesz kezelve egy ilyen tevékenység törzsén belül, ez a kivétel ugyanazt CancellationToken tartalmazza, amelyet a rendszer átad a tevékenységnek, és ez a jogkivonat azt mutatja, hogy a rendszer kéri a törlést.
Ha egy másik kivétel nem lesz kezelve a tevékenység törzsén belül, a tevékenység az Faulted állapotban végződik, és a tevékenységre való várakozásra vagy az eredmény elérésére tett kísérletek kivételt okoznak.
I/O-kötött tevékenységek
Ha olyan feladatot szeretne létrehozni, amelyet nem szabad közvetlenül háttérbe helyeznie egy szálnak a teljes végrehajtásához, használja a típust TaskCompletionSource<TResult> . Ez a típus egy Task társított Task<TResult> példányt visszaadó tulajdonságot tesz elérhetővé. A feladat életciklusát olyan módszerek vezérlik TaskCompletionSource<TResult> , mint SetResulta , SetException, SetCanceledés azok TrySet
változatai.
Tegyük fel, hogy olyan feladatot szeretne létrehozni, amely egy megadott idő elteltével fejeződik be. Előfordulhat például, hogy késleltetni szeretne egy tevékenységet a felhasználói felületen. Az System.Threading.Timer osztály már lehetővé teszi, hogy aszinkron módon meghívjon egy meghatalmazottat egy megadott idő elteltével, és a használatával TaskCompletionSource<TResult> előtérrel is beállíthatja Task<TResult> az időzítőt, például:
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
A Task.Delay metódus erre a célra van megadva, és egy másik aszinkron metóduson belül is használhatja, például egy aszinkron lekérdezési ciklus implementálásához:
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
Az TaskCompletionSource<TResult> osztály nem rendelkezik nem általános megfelelővel. Azonban származikTask, Task<TResult> így használhatja az általános TaskCompletionSource<TResult> objektumot az I/O-kötött metódusokhoz, amelyek egyszerűen visszaadnak egy feladatot. Ehhez használhatja a forrást egy próbabábuval TResult
(Boolean ez egy jó alapértelmezett választás, de ha aggódik a felhasználó Task miatt, hogy a lecímkésítést egy Task<TResult>magánjellegű TResult
típusra szeretné használni). Az előző példában szereplő metódus például Delay
az aktuális időt és az eredményül kapott eltolást (Task<DateTimeOffset>
) adja vissza. Ha egy ilyen eredményérték szükségtelen, a metódus a következőképpen kódozható (figyelje meg a visszatérési típus módosítását és az argumentum TrySetResultmódosítását):
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
Vegyes számítási és I/O-kötött tevékenységek
Az aszinkron metódusok nem csak számítási vagy I/O-kötött műveletekre korlátozódnak, hanem a kettő keverékét is jelenthetik. Valójában több aszinkron művelet gyakran nagyobb vegyes műveletekbe van kombinálva. Az előző példában szereplő metódus például RenderAsync
számításigényes műveletet hajtott végre egy kép valamilyen bemeneten imageData
alapuló rendereléséhez. Ez imageData
egy olyan webszolgáltatásból származhat, amelyhez aszinkron módon fér hozzá:
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
Ez a példa azt is bemutatja, hogyan lehet egyetlen lemondási jogkivonatot több aszinkron műveleten keresztül csoportosítani. További információkért tekintse meg a tevékenységalapú aszinkron minta használatának lemondási használati szakaszát.