Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Wanneer u een synchrone methode in een bibliotheek hebt, is het misschien verleidelijk om een asynchrone tegenhanger aan te bieden door deze in Task.Run te verpakken.
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
In dit artikel wordt uitgelegd waarom deze benadering bijna altijd verkeerd is voor bibliotheken en hoe u nadenkt over de compromissen.
Schaalbaarheid versus offloading
Asynchrone programmering biedt twee verschillende voordelen:
- Schaalbaarheid : verminder het resourceverbruik door threads te verwijderen tijdens I/O-wachttijden.
- Offloading : verplaats werk naar een andere thread om de reactiesnelheid te behouden (bijvoorbeeld om een UI-thread vrij te houden) of parallelle uitvoering te bereiken.
Deze voordelen vereisen verschillende benaderingen. Het kritieke onderscheid: het verpakken van een synchrone methode in Task.Run helpt met het overdragen van taken, maar doet niets voor schaalbaarheid.
Waarom Task.Run de schaalbaarheid niet verbetert
Een echt asynchrone implementatie vermindert het aantal threads dat wordt gebruikt tijdens een langdurige bewerking. Een Task.Run wrapper blokkeert nog steeds een thread. Hiermee wordt de blokkering van de ene thread naar de andere verplaatst:
public static class TimerExampleWrong
{
public static Task SleepAsync(int millisecondsTimeout)
{
return Task.Run(() => Thread.Sleep(millisecondsTimeout));
}
}
Public Module TimerExampleWrong
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
End Function
End Module
Vergelijk deze benadering met een echt asynchrone implementatie die geen threads verbruikt tijdens het wachten:
public static class TimerExampleRight
{
public static Task SleepAsync(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<bool>();
var timer = new Timer(
_ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);
tcs.Task.ContinueWith(
_ => timer.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
Public Module TimerExampleRight
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim tmr As New Timer(
Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)
tcs.Task.ContinueWith(
Sub(t) tmr.Dispose(), TaskScheduler.Default)
Return tcs.Task
End Function
End Module
Beide implementaties zijn voltooid na de opgegeven vertraging, maar de tweede implementatie blokkeert geen thread tijdens het wachten. Voor servertoepassingen die veel gelijktijdige aanvragen verwerken, heeft dat verschil rechtstreeks invloed op het aantal aanvragen dat een server tegelijkertijd kan verwerken.
Het uitladen is de verantwoordelijkheid van de consument
Synchrone aanroepen in Task.Run verpakken is handig om werk van een gebruikersinterface-thread te verplaatsen. De consument, niet de bibliotheek, moet echter deze omsluiting afhandelen.
public static class UIOffloadExample
{
public static int ComputeIntensive(int input)
{
int result = 0;
for (int i = 0; i < input; i++)
{
result += i;
}
return result;
}
public static async Task ConsumeFromUIThreadAsync()
{
int result = await Task.Run(() => ComputeIntensive(10_000));
Console.WriteLine($"Result: {result}");
}
}
Public Module UIOffloadExample
Public Function ComputeIntensive(input As Integer) As Integer
Dim result As Integer = 0
For i As Integer = 0 To input - 1
result += i
Next
Return result
End Function
Public Async Function ConsumeFromUIThreadAsync() As Task
Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
Console.WriteLine($"Result: {result}")
End Function
End Module
De consument kent de context: of ze zich in een UI-thread bevinden, hoeveel granulariteit ze nodig hebben en of offloading waarde toevoegt. De bibliotheek doet het niet.
Waarom bibliotheken geen asynchrone synchronisatie-wrappers beschikbaar moeten maken
Wanneer een bibliotheek alleen de synchrone methode (en niet een asynchrone wrapper) beschikbaar maakt, profiteren consumenten op verschillende manieren:
- Verminderd API-oppervlak: minder methoden om te leren, te testen en te onderhouden.
- Geen misleidende schaalbaarheids verwachtingen: gebruikers weten dat alleen de methoden die als asynchroon worden weergegeven, daadwerkelijk schaalbaarheidsvoordelen bieden.
-
Consumentencontrole: bellers kiezen of en hoe ze het offloaden uitvoeren, op het juiste niveau van granulariteit. Een servertoepassing met hoge doorvoer kan de synchrone methode direct aanroepen, waardoor onnodige overhead van
Task.Runwordt vermeden. - Betere prestaties: Asynchrone wrappers voegen overhead toe via toewijzingen, contextswitches en threadpool-scheduling. Voor fijnmazige bewerkingen kan die overhead aanzienlijk zijn.
Uitzonderingen op de regel
Sommige basisklassen maken asynchrone methoden beschikbaar, zodat afgeleide klassen deze kunnen overschrijven met echt asynchrone implementaties. De basisklasse biedt een async-over-sync-standaard.
Bijvoorbeeld stelt StreamReadAsync en WriteAsync bloot. De basis-implementaties verpakken de synchrone Read en Write methoden. Afgeleide klassen zoals FileStream en NetworkStream overschrijven deze methoden met asynchrone I/O-implementaties die echte schaalbaarheidsvoordelen bieden.
Op dezelfde manier biedt TextReaderReadToEndAsync de basisklasse als een wrapper, en StreamReader overschrijft deze met een echt asynchrone implementatie die intern ReadAsync aanroept.
Deze uitzonderingen zijn geldig omdat:
- Het patroon is ontworpen voor polymorfisme. Gebruikers communiceren met het basistype.
- Afgeleide typen bieden echt asynchrone overrides.
Richtlijn
Maak asynchrone methoden alleen beschikbaar vanuit een bibliotheek wanneer de implementatie echte schaalbaarheidsvoordelen biedt ten opzichte van de synchrone tegenhanger. Maak geen asynchrone methoden beschikbaar voor offloading. Laat die keuze aan de consument over.