Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
När du har en synkron metod i ett bibliotek kan du vara frestad att exponera en asynkron motsvarighet som omsluter den i Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
Den här artikeln förklarar varför den metoden nästan alltid är fel för bibliotek och hur man tänker på kompromisserna.
Skalbarhet jämfört med avlastning
Asynkron programmering ger två distinkta fördelar:
- Skalbarhet – Minska resursförbrukningen genom att frigöra trådar under I/O-väntetider.
- Avlastning – Flytta arbetet till en annan tråd för att bibehålla svarstiden (till exempel att hålla en användargränssnittstråd fri) eller uppnå parallellitet.
Dessa fördelar kräver olika metoder. Den kritiska skillnaden: att omsluta en synkron metod i Task.Run hjälper till med avlastning men gör ingenting för skalbarhet.
Varför Task.Run förbättrar inte skalbarheten
En verkligt asynkron implementering minskar antalet trådar som förbrukas under en långvarig åtgärd. En Task.Run omslutning blockerar fortfarande en tråd – den flyttar bara blockeringen från en tråd till en annan:
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
Jämför den metoden med en verkligt asynkron implementering som inte förbrukar några trådar i väntan:
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
Båda implementeringarna slutförs efter den angivna fördröjningen, men den andra implementeringen blockerar inte någon tråd i väntan. För serverprogram som hanterar många samtidiga begäranden påverkar den skillnaden direkt hur många begäranden en server kan bearbeta samtidigt.
Avlastning är konsumentens ansvar
Att omsluta synkrona anrop i Task.Run är användbart för att avlasta arbete från en användargränssnittstråd. Konsumenten, inte biblioteket, bör dock hantera den här inpackningen:
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
Användaren känner till sin kontext: om de är på en UI-tråd, hur mycket detaljnivå de behöver och om offloading tillför värde. Biblioteket gör det inte.
Varför bibliotek inte ska exponera asynkron-över-synkrona omslutningar
När ett bibliotek endast exponerar den synkrona metoden (och inte en asynkron omslutning) drar konsumenterna nytta av det på flera sätt:
- Minskad API-yta: Färre metoder för att lära sig, testa och underhålla.
- Inga missvisande skalbarhetsförväntningar: Användarna vet att endast de metoder som exponeras som asynkrona faktiskt ger skalbarhetsfördelar.
-
Konsumentkontroll: Uppringare väljer om och hur de ska hantera avlastning, på rätt nivå av detaljering. Ett serverprogram med högt dataflöde kan anropa den synkrona metoden direkt, vilket undviker onödiga omkostnader från
Task.Run. - Bättre prestanda: Asynkrona omslutningar lägger till omkostnader via allokeringar, kontextväxlar och schemaläggning av trådpooler. För detaljerade åtgärder kan dessa omkostnader vara betydande.
Undantag till regeln
Vissa basklasser exponerar asynkrona metoder så att härledda klasser kan åsidosätta dem med verkligt asynkrona implementeringar. Basklassen tillhandahåller en standardinställning för async-over-sync.
Till exempel Stream exponerar ReadAsync och WriteAsync. Basimplementeringarna omsluter synkrona Read metoder och Write metoder. Härledda klasser som FileStream och NetworkStream åsidosätter dessa metoder med asynkrona I/O-implementeringar som ger verkliga skalbarhetsfördelar.
TextReader På samma sätt tillhandahåller ReadToEndAsync basklassen som omslutning och StreamReader åsidosätter den med en verkligt asynkron implementering som anropar ReadAsync internt.
Dessa undantag är giltiga eftersom:
- Mönstret är utformat för polymorfism. Anropare interagerar med den basiska typen.
- Härledda typer medför verkligt asynkrona överskrivningar.
Riktlinjer
Exponera asynkrona metoder från ett bibliotek endast när implementeringen ger verkliga skalbarhetsfördelar jämfört med dess synkrona motsvarighet. Exponera inte asynkrona metoder enbart för avlastning. Lämna det valet till konsumenten.