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 eine Bibliothek nur asynchrone APIs verfügbar macht, schließen Verbraucher sie manchmal in synchrone Aufrufe um, um eine synchrone Schnittstelle oder einen Vertrag zu erfüllen. Dieses "sync-over-async"-Muster kann einfach erscheinen, aber es ist eine häufige Quelle von Deadlocks und Leistungsproblemen.
Grundlegende Umbruchmuster
Ein synchroner Wrapper um eine aufgabenbasierte asynchrone Mustermethode (TAP) greift auf die Eigenschaft der Result Aufgabe zu, die den aufrufenden Thread blockiert:
public class TapWrapper
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
}
Public Module TapWrapper
Public Function Foo(fooAsync As Func(Of Task(Of Integer))) As Integer
Return fooAsync().Result
End Function
End Module
Dieser Ansatz sieht einfach aus, kann aber in Abhängigkeit von der Umgebung, in der sie ausgeführt wird, schwerwiegende Probleme verursachen.
Deadlocks mit Single-Thread-Umgebungen
Das gefährlichste Szenario tritt auf, wenn Sie einen synchronen Wrapper von einem Thread aufrufen, der ein Einzelthread ist SynchronizationContext. Dieses Szenario ist in der Regel ein UI-Thread in WPF-, Windows Forms- oder MAUI-Anwendungen.
public static class DeadlockExample
{
private static void Delay(int milliseconds)
{
DelayAsync(milliseconds).Wait();
}
private static async Task DelayAsync(int milliseconds)
{
await Task.Delay(milliseconds);
}
}
Public Module DeadlockExample
Private Sub Delay(milliseconds As Integer)
DelayAsync(milliseconds).Wait()
End Sub
Private Async Function DelayAsync(milliseconds As Integer) As Task
Await Task.Delay(milliseconds)
End Function
End Module
Hier erfahren Sie, was schritt für Schritt geschieht:
- Der UI-Thread ruft
Delayauf, dasDelayAsync(milliseconds).Wait()aufruft. -
DelayAsyncwird synchron ausgeführt, bisawait Task.Delay(milliseconds)erreicht ist. - Da die Verzögerung noch nicht abgeschlossen ist, erfasst
awaitden aktuellen SynchronizationContext und hält an.DelayAsyncgibt einen Task an den Aufrufer zurück. - Der UI-Thread blockiert in
.Wait(), während er auf den Abschluss dieser Aufgabe wartet. - Nach Abschluss der Verzögerung muss die Fortsetzung im ursprünglichen
SynchronizationContextUI-Thread ausgeführt werden. - Der UI-Thread kann die Fortsetzung nicht verarbeiten, da er in
.Wait()blockiert ist. - Deadlock.
Von Bedeutung
Der Erfolg oder Fehler von Sync-over-async-Code hängt von der Umgebung ab, in der sie ausgeführt wird. Code, der in einer Konsolen-App funktioniert, kann in einem UI-Thread oder in ASP.NET (unter .NET Framework) inaktiv werden. Diese Umgebungsabhängigkeit ist ein wesentlicher Grund, um synchrone Wrapper nicht verfügbar zu machen.
Erschöpfung des Threadpools
Deadlocks sind nicht auf UI-Threads beschränkt. Wenn eine asynchrone Methode vom Threadpool abhängt, um ihre Arbeit abzuschließen, z. B. indem es einen letzten Verarbeitungsschritt in die Warteschlange stellt, kann das Blockieren vieler Pool-Threads mit synchronen Wrappern den Pool überfordern.
public static class ThreadPoolDeadlockExample
{
public static int Foo(Func<Task<int>> fooAsync)
{
return fooAsync().Result;
}
public static async Task DemonstrateDeadlockRiskAsync()
{
var tasks = Enumerable.Range(0, 25)
.Select(_ => Task.Run(() => Foo(() => SomeIOOperationAsync())));
await Task.WhenAll(tasks);
}
private static async Task<int> SomeIOOperationAsync()
{
await Task.Delay(100);
return 42;
}
}
Szenario:
- Viele Threadpool-Threads rufen
Fooauf, was in.Resultblockiert. - Jeder asynchrone Vorgang schließt seine E/A ab und benötigt einen Threadpool-Thread, um den Abschlussrückruf auszuführen.
- Da blockierte Aufrufe verfügbare Worker-Threads belegen, warten Abschlüsse möglicherweise lange, bis ein Thread verfügbar wird.
- Moderne .NET kann im Laufe der Zeit mehr Threads im Threadpool hinzufügen, aber die Anwendung kann weiterhin unter schwerer Ressourcenknappheit im Threadpool, schlechtem Durchsatz, langen Verzögerungen oder einem offensichtlichen Hänger leiden.
Dieses Muster hat HttpWebRequest.GetResponse in .NET Framework 1.x beeinflusst, wobei die synchrone Methode als Wrapper um die asynchrone BeginGetResponse/EndGetResponse implementiert wurde.
Richtlinie: Vermeidung der Offenlegung synchroner Wrapper
Machen Sie keine synchrone Methode verfügbar, die eine asynchrone Implementierung umschließt. Lassen Sie stattdessen die Entscheidung, ob etwas blockiert werden soll, beim Verbraucher. Der Nutzer kennt seine Thread-Umgebung und kann eine fundierte Wahl treffen.
Wenn Sie feststellen, dass Sie eine asynchrone Methode synchron aufrufen müssen, überlegen Sie zuerst, ob Sie den Code so umstrukturieren können, dass er konsequent asynchron bleibt. Refactoring ist oft die bessere langfristige Lösung.
Minderungsstrategien, wenn Sync-over-Async unvermeidbar ist
Manchmal ist sync-over-async wirklich unvermeidbar. Beispielsweise ist es unvermeidbar, wenn Sie eine Schnittstelle implementieren, die eine synchrone Methode erfordert, und die einzige verfügbare Implementierung ist asynchron. Wenden Sie in diesen Fällen die folgenden Strategien an, um das Risiko zu reduzieren.
Verwenden ConfigureAwait(false) in der asynchronen Implementierung
Wenn Sie die asynchrone Methode steuern, verwenden Sie Task.ConfigureAwait zusammen mit false bei jedem await, um zu verhindern, dass die Fortsetzung ins ursprüngliche SynchronizationContext zurücküberführt wird.
public static class ConfigureAwaitMitigation
{
public static async Task<int> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
return 42;
}
public static int Sync()
{
return LibraryMethodAsync().GetAwaiter().GetResult();
}
}
Public Module ConfigureAwaitMitigation
Public Async Function LibraryMethodAsync() As Task(Of Integer)
Await Task.Delay(100).ConfigureAwait(False)
Return 42
End Function
Public Function Sync() As Integer
Return LibraryMethodAsync().Result
End Function
End Module
Als Bibliotheksautor sollten Sie ConfigureAwait(false) für alle 'await'-Anweisungen verwenden, es sei denn, Ihr Code muss speziell im erfassten Kontext fortgesetzt werden. Die Verwendung ConfigureAwait(false) ist eine bewährte Methode für die Leistung und hilft, Deadlocks zu verhindern, wenn Verbraucher blockieren.
Auslagern in den Threadpool
Wenn Sie die asynchrone Implementierung nicht steuern – und sie möglicherweise nicht ConfigureAwait(false) verwendet – lagern Sie den Aufruf an den Thread-Pool aus. Der Threadpool hat keinen SynchronizationContext, sodass der Await-Vorgang nicht versucht, zu einem blockierten Thread zurückzukehren.
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Testen in mehreren Umgebungen
Wenn Sie einen synchronen Wrapper versenden müssen, testen Sie ihn von:
- Ein UI-Thread (WPF, Windows Forms).
- Der Threadpool ist unter Last.
- Der Threadpool mit einer niedrigen maximalen Threadanzahl.
- Eine Konsolenanwendung.
Verhalten, das in einer Umgebung funktioniert, kann in einer anderen Umgebung blockiert sein.