Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Pokud máte v knihovně synchronní metodu, může být lákavé zveřejnit asynchronní protějšek, který ji zabalí do Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
Tento článek vysvětluje, proč je tento přístup téměř vždy nesprávný pro knihovny a jak přemýšlet o kompromisech.
Škálovatelnost versus snižování zátěže
Asynchronní programování poskytuje dvě různé výhody:
- Škálovatelnost – Snížení spotřeby prostředků uvolněním vláken během čekání na vstupně-výstupní operace
- Přesměrování zpracování – Přesunutí práce na jiné vlákno za účelem zachování rychlosti odezvy (například zachování volného vlákna uživatelského rozhraní) nebo dosažení paralelismu.
Tyto výhody vyžadují různé přístupy. Zásadní rozdíl: zabalení synchronní metody do Task.Run pomáhá s snižováním zátěže, ale nedělá nic pro škálovatelnost.
Proč Task.Run nezlepšuje škálovatelnost
Skutečně asynchronní implementace snižuje počet vláken spotřebovaných během dlouhotrvající operace. Zabalovač Task.Run stále blokuje vlákno – pouze přesune blokování z jednoho vlákna na druhé.
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
Porovnejte tento přístup se skutečně asynchronní implementací, která při čekání nespotřebovává žádná vlákna.
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
Obě implementace se dokončí po zadaném zpoždění, ale druhá implementace během čekání neblokuje žádné vlákno. U serverových aplikací, které zpracovávají mnoho souběžných požadavků, tento rozdíl přímo ovlivňuje, kolik požadavků může server zpracovat současně.
Snižování zátěže je odpovědností spotřebitele.
Zabalení synchronních volání do Task.Run je užitečné pro odlehčení práce z vlákna uživatelského rozhraní. Příjemce, nikoli knihovna, by však měl zpracovat toto zabalení:
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
Spotřebitel zná svůj kontext: zda se nachází ve vlákně uživatelského rozhraní, jakou úroveň podrobnosti potřebuje a zda přesun zátěže přidává hodnotu. Knihovna ne.
Proč by knihovny neměly vystavit obálky asynchronního přepisu synchronizace
Když knihovna zpřístupňuje pouze synchronní metodu (a ne asynchronní obálku), uživatelé z toho mohou těžit několika způsoby:
- Zmenšená plocha rozhraní API: Méně metod, jak se učit, testovat a udržovat.
- Žádná zavádějící očekávání škálovatelnosti: Uživatelé vědí, že pouze metody vystavené jako asynchronní ve skutečnosti poskytují výhody škálovatelnosti.
-
Kontrola spotřebitele: Uživatelé si volí , zda a jak se má provádět odkládání, na správné úrovni granularity. Serverová aplikace s vysokou propustností může přímo volat synchronní metodu a vyhnout se zbytečným režijním nákladům .
Task.Run - Lepší výkon: Asynchronní obálky zvyšují režijní náklady prostřednictvím alokací, přepínání kontextů a plánování v rámci vlákna. U jemně odstupňovaných operací může být tato režie významná.
Výjimky pravidla
Některé základní třídy zpřístupňují asynchronní metody, aby je odvozené třídy mohly přepsat skutečně asynchronními implementacemi. Základní třída poskytuje výchozí asynchronní přesynchronní synchronizaci.
Například Stream zveřejňuje ReadAsync a WriteAsync. Základní implementace obalují synchronní metody Read a Write. Odvozené třídy jako FileStream a NetworkStream přepisují tyto metody asynchronními vstupně-výstupními implementacemi, které poskytují skutečné výhody škálovatelnosti.
Podobně TextReader poskytuje ReadToEndAsync jako obálku nad základní třídou a StreamReader ji přepisuje skutečně asynchronní implementací, která volá ReadAsync interně.
Tyto výjimky jsou platné, protože:
- Vzor je určen pro polymorfismus. Volající pracují s základním typem.
- Odvozené typy poskytují skutečně asynchronní přepsání.
Směrnice
Zveřejněte asynchronní metody z knihovny jen když implementace poskytuje skutečné výhody škálovatelnosti oproti synchronnímu protějšku. Nezpřístupňujte asynchronní metody čistě pro snižování zátěže. Ponechte tuhle volbu příjemci.