Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Wenn Sie über eine synchrone Methode in einer Bibliothek verfügen, sind Sie möglicherweise versucht, ein asynchrones Gegenstück verfügbar zu machen, das sie umschließt:Task.Run
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
In diesem Artikel wird erläutert, warum dieser Ansatz für Bibliotheken fast immer falsch ist und wie man über die Kompromisse nachdenken kann.
Skalierbarkeit und Offloading
Die asynchrone Programmierung bietet zwei unterschiedliche Vorteile:
- Skalierbarkeit – Verringern Sie den Ressourcenverbrauch, indem Sie Threads während der E/A-Wartezeit freigeben.
- Auslagern – Verschieben Sie Arbeit in einen anderen Thread, um die Reaktionsfähigkeit aufrechtzuerhalten (z. B. einen UI-Thread frei zu halten) oder eine Parallelität zu erzielen.
Diese Vorteile erfordern unterschiedliche Ansätze. Der kritische Unterschied: Das Umschließen einer synchronen Methode Task.Run hilft beim Auslagern, trägt jedoch nichts zur Skalierbarkeit bei.
Warum Task.Run verbessert sich die Skalierbarkeit nicht
Eine wirklich asynchrone Implementierung reduziert die Anzahl der Threads, die während eines lang ausgeführten Vorgangs verbraucht werden. Ein Task.Run Wrapper blockiert weiterhin einen Thread – er verschiebt lediglich die Blockierung von einem Thread in einen anderen:
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
Vergleichen Sie diesen Ansatz mit einer wirklich asynchronen Implementierung, die beim Warten keine Threads verbraucht:
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 Implementierungen werden nach der angegebenen Verzögerung abgeschlossen, die zweite Implementierung blockiert jedoch keinen Thread beim Warten. Bei Serveranwendungen, die viele gleichzeitige Anforderungen verarbeiten, wirkt sich dieser Unterschied direkt darauf aus, wie viele Anforderungen ein Server gleichzeitig verarbeiten kann.
Das Ausladen liegt in der Verantwortung des Verbrauchers.
Das Umschließen synchroner Aufrufe in Task.Run ist nützlich, um Arbeit aus einem UI-Thread auszulagern. Der Consumer, nicht die Bibliothek, sollte jedoch diese Verpackung behandeln.
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
Der Verbraucher kennt deren Kontext: ob sie sich in einem UI-Thread befinden, wie viel Granularität sie benötigen und ob das Auslagern Mehrwert bringt. Die Bibliothek tut es nicht.
Warum Bibliotheken async-over-sync wrapper nicht verfügbar machen sollten
Wenn eine Bibliothek nur die synchrone Methode (und nicht ein asynchroner Wrapper) verfügbar macht, profitieren Verbraucher auf verschiedene Arten:
- Reduzierter API-Oberflächenbereich: Weniger Methoden zum Erlernen, Testen und Warten.
- Keine irreführenden Skalierbarkeitserwartungen: Benutzer wissen, dass nur die Methoden, die als asynchron verfügbar gemacht werden, Skalierbarkeitsvorteile bieten.
-
Verbrauchersteuerung: Anrufer wählen aus, ob und wie sie auslagern, auf der geeigneten Granularitätsebene. Eine Serveranwendung mit hohem Durchsatz kann die synchrone Methode direkt aufrufen und unnötigen Aufwand vermeiden
Task.Run. - Bessere Leistung: Asynchrone Wrapper fügen Mehraufwand durch Zuordnungen, Kontextschalter und Threadpoolplanung hinzu. Bei feinkörnigen Vorgängen kann dieser Aufwand erheblich sein.
Ausnahmen von der Regel
Einige Basisklassen machen asynchrone Methoden verfügbar, sodass abgeleitete Klassen sie mit wirklich asynchronen Implementierungen überschreiben können. Die Basisklasse bietet standardmäßig eine async-over-sync Implementierung.
Beispielsweise stellt StreamReadAsync und WriteAsync bereit. Die Basisimplementierungen umschließen die synchronen Read und Write Methoden. Abgeleitete Klassen wie FileStream und NetworkStream überschreiben diese Methoden mit asynchronen E/A-Implementierungen, die echte Skalierbarkeitsvorteile bieten.
Ähnlich stellt TextReader in der Basisklasse als Wrapper bereit, und StreamReader überschreibt es mit einer wirklich asynchronen Implementierung, die intern ReadAsync aufruft.
Diese Ausnahmen sind gültig, da:
- Das Muster wurde für Polymorphismus entwickelt. Anrufer interagieren mit dem Basistyp.
- Abgeleitete Typen bieten echte asynchrone Überschreibungen an.
Vorgabe
Machen Sie asynchrone Methoden aus einer Bibliothek nur verfügbar, wenn die Implementierung echte Skalierbarkeitsvorteile gegenüber dem synchronen Gegenstück bietet. Machen Sie keine asynchronen Methoden nur zum Entladen verfügbar. Lassen Sie diese Wahl dem Verbraucher überlassen.