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 een bibliotheek alleen asynchrone API's beschikbaar maakt, verpakken consumenten deze soms in synchrone aanroepen om te voldoen aan een synchrone interface of contract. Dit patroon 'sync-over-async' kan eenvoudig lijken, maar het is een veelvoorkomende bron van impasses en prestatieproblemen.
Basisomwikkelingspatronen
Een synchrone wrapper rond een Task-gebaseerde Asynchrone Patroonmethode (TAP) benadert de Result-eigenschap van de taak, waardoor de aanroepende thread geblokkeerd raakt.
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
Deze benadering ziet er eenvoudig uit, maar kan ernstige problemen veroorzaken, afhankelijk van de omgeving waarin deze wordt uitgevoerd.
Deadlocks met enkelvoudige contexten
Het gevaarlijkste scenario treedt op wanneer u een synchrone wrapper aanroept vanuit een thread die enkelvoudig is SynchronizationContext. Dit scenario is doorgaans een UI-thread in WPF-, Windows Forms- of MAUI-toepassingen.
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
Dit gebeurt stap voor stap:
- De UI-thread roept
Delayaan, dat op zijn beurtDelayAsync(milliseconds).Wait()aanroept. -
DelayAsyncwordt synchroon uitgevoerd totdat het bereiktawait Task.Delay(milliseconds). - Omdat de vertraging nog niet is voltooid,
awaitwordt de huidige SynchronizationContext vastgelegd en onderbroken.DelayAsyncretourneert een Task aan de aanroeper. - De UI-thread blokkeert in
.Wait(), terwijl het wacht tot die taak is voltooid. - Wanneer de vertraging is voltooid, moet de uitvoering doorgaan op de oorspronkelijke
SynchronizationContext-thread, wat de UI-thread is. - De UI-thread kan de voortzetting niet verwerken omdat deze is geblokkeerd in
.Wait(). - Impasse.
Belangrijk
Het succes of falen van sync-over-async code is afhankelijk van de omgeving waarin het wordt uitgevoerd. Code die in een console-app werkt, kan een impasse opleveren voor een UI-thread of in ASP.NET (op .NET Framework). Deze omgevingsafhankelijkheid is een belangrijke reden om te vermijden dat synchrone wrappers blootgesteld worden.
Uitputting van threadpool
Impasses zijn niet beperkt tot UI-threads. Als een asynchrone methode afhankelijk is van de threadpool om het werk te voltooien, bijvoorbeeld door een laatste verwerkingsstap in de wachtrij te plaatsen, kan het blokkeren van veel draadpools met synchrone wrappers leiden tot het leegtrekken van de pool.
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;
}
}
In dit scenario:
- Veel thread pool threads roepen
Fooaan, dat in.Resultblokkeert. - Elke asynchrone bewerking voltooit de I/O en heeft een thread pool nodig om de voltooingscallback uit te voeren.
- Omdat geblokkeerde aanroepen beschikbare werkrolthreads in beslag nemen, kunnen voltooiingen lang wachten tot een thread beschikbaar is.
- Moderne .NET kan na verloop van tijd meer draadpoolthreads toevoegen, maar de toepassing kan nog steeds ernstige threadpool-uitputting, slechte doorvoer, lange vertragingen of een ogenschijnlijke vastloper ondervinden.
Dit patroon beïnvloedde HttpWebRequest.GetResponse in .NET Framework 1.x, waarbij de synchrone methode werd geïmplementeerd als een wrapper rond de asynchrone BeginGetResponse/EndGetResponse.
Richtlijn: Vermijd het blootstellen van synchrone wrappers
Maak geen synchrone methode beschikbaar die een asynchrone implementatie verpakt. Laat in plaats daarvan de beslissing of er moet worden geblokkeerd aan de gebruikers over. De consument kent hun draadomgeving en kan een geïnformeerde keuze maken.
Als u merkt dat u een asynchrone methode synchroon moet aanroepen, bedenk dan eerst of u de code kunt herstructureren, zodat deze volledig asynchroon is. Herstructureren is vaak de betere lange-termijnoplossing.
Risicobeperkingsstrategieën wanneer sync-over-async onvermijdelijk is
Soms is sync-over-async echt onvermijdelijk. Het is bijvoorbeeld onvermijdelijk wanneer u een interface implementeert waarvoor een synchrone methode is vereist en de enige beschikbare implementatie asynchroon is. In dergelijke gevallen past u de volgende strategieën toe om het risico te verminderen.
Gebruiken ConfigureAwait(false) in de asynchrone implementatie
Als u de asynchrone methode beheert, gebruikt Task.ConfigureAwait met false op elke await om te voorkomen dat de voortzetting wordt gemarshald naar het oorspronkelijke SynchronizationContext.
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 auteur van een bibliotheek, gebruik ConfigureAwait(false) op alle await-uitdrukkingen, tenzij uw code specifiek moet worden hervat in de vastgelegde context. Het gebruik van ConfigureAwait(false) is een best practice voor prestaties en helpt deadlocks te voorkomen wanneer consumenten blokkeren.
Overdragen naar de threadpool
Als u geen controle hebt over de asynchrone implementatie (en deze mogelijk niet gebruikt ConfigureAwait(false)), moet u de aanroep naar de threadgroep offloaden. De threadpool heeft geen SynchronizationContext, dus 'await' zal niet proberen terug te keren naar een geblokkeerde thread.
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 meerdere omgevingen
Als u een synchrone wrapper moet verzenden, test u deze vanuit:
- Een UI-thread (WPF, Windows Forms).
- De threadpool staat onder belasting.
- De threadpool met een laag maximumaantal threads.
- Een consoletoepassing.
Gedrag dat in de ene omgeving werkt, kan in een andere omgeving tot een deadlock leiden.