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 ett bibliotek endast exponerar asynkrona API:er omsluter konsumenterna dem ibland i synkrona anrop för att uppfylla ett synkront gränssnitt eller kontrakt. Det här "sync-over-async"-mönstret kan verka enkelt, men det är en vanlig källa till dödlägen och prestandaproblem.
Grundläggande omslutningsmönster
En synkron omslutning av en uppgiftsbaserad Task-based Asynchronous Pattern (TAP)-metod kommer åt uppgiftens Result-egenskap, vilket blockerar den anropande tråden.
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
Den här metoden ser enkel ut, men den kan orsaka allvarliga problem beroende på vilken miljö den körs i.
Dödlägen med entrådade kontexter
Det farligaste scenariot inträffar när du anropar en synkron omslutning från en tråd som har en enkeltrådad SynchronizationContext. Det här scenariot är vanligtvis en användargränssnittstråd i WPF-, Windows Forms- eller MAUI-program.
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
Så här händer steg för steg:
- Användargränssnittstråden anropar
Delay, som anroparDelayAsync(milliseconds).Wait(). -
DelayAsynckörs synkront tills den nårawait Task.Delay(milliseconds). - Eftersom fördröjningen inte är klar ännu,
awaitregistrerar den aktuella SynchronizationContext och pausar.DelayAsyncreturnerar en Task till anroparen. - UI-tråden blockeras i
.Wait()och väntar på att uppgiften ska slutföras. - När fördröjningen är klar behöver fortsättningen köras på den ursprungliga
SynchronizationContext, som är användargränssnittstråden. - Användargränssnittstråden kan inte bearbeta fortsättningen eftersom den är blockerad i
.Wait(). - Dödläge.
Viktigt!
Om sync-over-async-koden lyckas eller misslyckas beror det på vilken miljö den körs i. Kod som fungerar i en konsolapp kan vara låst i en användargränssnittstråd eller i ASP.NET (på .NET Framework). Det här miljöberoendet är en viktig orsak till att undvika att exponera synkrona omslutningar.
Trådpoolsöverbelastning
Dödlägen är inte begränsade till gränssnittstrådar. Om en asynkron metod är beroende av trådpoolen för att slutföra sitt arbete, till exempel genom att köa ett sista bearbetningssteg, kan blockering av många pooltrådar med synkrona omslutningar leda till resursbrist i poolen:
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;
}
}
I det här scenariot:
- Många trådar i trådpoolen anropar
Foo, som blockerar i.Result. - Varje asynkron operation slutför sin I/O och behöver en tråd från trådpoolen för att köra sin återanropsfunktion.
- Eftersom blockerade anrop upptar tillgängliga arbetstrådar kan slutföranden vänta länge tills en tråd blir tillgänglig.
- Moderna .NET kan lägga till fler trådpooltrådar över tid, men programmet kan fortfarande drabbas av svår trådpoolssvält, dåligt dataflöde, långa fördröjningar eller en uppenbar hängning.
Det här mönstret påverkade HttpWebRequest.GetResponse i .NET Framework 1.x, där den synkrona metoden implementerades som omslutning runt den asynkrona BeginGetResponse/EndGetResponse.
Riktlinje: Undvik att exponera synkrona omslutningar
Exponera inte en synkron metod som omsluter en asynkron implementering. Lämna i stället beslutet om att blockera för konsumenten. Konsumenten känner till sin trådmiljö och kan göra ett välgrundat val.
Om du behöver anropa en asynkron metod synkront bör du först överväga om du kan omstrukturera koden så att den är "asynkron hela vägen nedåt". Refaktorisering är ofta den bättre långsiktiga lösningen.
Åtgärdsstrategier när synkronisering över asynkronisering inte kan undvikas
Ibland är det verkligen oundvikligt att använda synkronisering över asynkrona metoder. Det är till exempel oundvikligt när du implementerar ett gränssnitt som kräver en synkron metod och den enda tillgängliga implementeringen är asynkron. I dessa fall använder du följande strategier för att minska risken.
Använda ConfigureAwait(false) i asynkron implementering
Om du styr async-metoden, använd Task.ConfigureAwait med false vid varje await för att förhindra att fortsättningen återställs till den ursprungliga 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
Som biblioteksutvecklare, använd ConfigureAwait(false) på alla väntningar om inte din kod specifikt behöver återupptas i den sparade kontexten. Att använda ConfigureAwait(false) är bästa praxis för prestanda och hjälper till att förhindra dödlägen när konsumenter blockerar.
Avlasta till trådpoolen
Om du inte kontrollerar asynkron implementeringen (och den kanske inte använder ConfigureAwait(false)) läser du av anropet till trådpoolen. Trådpoolen har ingen SynchronizationContext, så inväntningen kommer inte att försöka hoppa tillbaka till en blockerad tråd:
public int Sync()
{
return Task.Run(() => Library.FooAsync()).Result;
}
Public Function Sync() As Integer
Return Task.Run(Function() Library.FooAsync()).Result
End Function
Testa i flera miljöer
Om du måste skicka en synkron omslutning testar du den från:
- En användargränssnittstråd (WPF, Windows Forms).
- Trådpoolen under belastning.
- Trådpoolen med ett lågt maximalt antal trådar.
- Ett konsolprogram.
Beteende som fungerar i en miljö kan vara låst i en annan.