ExecutionContext en SynchronizationContext

Wanneer u werkt met async en await, spelen twee contexttypen belangrijke maar zeer verschillende rollen: ExecutionContext en SynchronizationContext. Je leert wat ieder doet, hoe ieder met async/await interageert, en waarom SynchronizationContext.Current niet over afwachtingspunten stroomt.

Wat is ExecutionContext?

ExecutionContext is een container voor omgevingsstatus die stroomt met de logische controlestroom van uw programma. In een synchrone wereld bevindt omgevingsinformatie zich in TLS (Thread-Local Storage) en alle code die op een bepaalde thread wordt uitgevoerd, ziet die gegevens. In een asynchrone wereld kan een logische bewerking beginnen op één thread, onderbreken en hervatten op een andere thread. Thread-lokale gegevens worden niet automatisch gevolgd—ExecutionContext zorgt ervoor dat ze wel worden gevolgd.

Hoe ExecutionContext stroomt

Vastleggen ExecutionContext met behulp van ExecutionContext.Capture(). Herstel deze tijdens de uitvoering van een gemachtigde met behulp van ExecutionContext.Run:

static void ExecutionContextCaptureDemo()
{
    // Capture the current ExecutionContext
    ExecutionContext? ec = ExecutionContext.Capture();

    // Later, run a delegate within that captured context
    if (ec is not null)
    {
        ExecutionContext.Run(ec, _ =>
        {
            // Code here sees the ambient state from the point of capture
            Console.WriteLine("Running inside captured ExecutionContext.");
        }, null);
    }
}
Sub ExecutionContextCaptureExample()
    ' Capture the current ExecutionContext
    Dim ec As ExecutionContext = ExecutionContext.Capture()

    ' Later, run a delegate within that captured context
    If ec IsNot Nothing Then
        ExecutionContext.Run(ec,
            Sub(state)
                ' Code here sees the ambient state from the point of capture
                Console.WriteLine("Running inside captured ExecutionContext.")
            End Sub, Nothing)
    End If
End Sub

Alle asynchrone API's in .NET die fork werken: Run, QueueUserWorkItem, BeginRead en anderen: leg ExecutionContext vast en gebruik de opgeslagen context bij het aanroepen van uw callback. Dit proces van het vastleggen van de status op de ene thread en het herstellen ervan op een andere is wat 'flowing ExecutionContext' betekent.

Wat is SynchronizationContext?

SynchronizationContext is een abstractie die een doelomgeving vertegenwoordigt waar u wilt werken. Verschillende UI-frameworks bieden hun eigen implementaties:

  • Windows Forms biedt WindowsFormsSynchronizationContext aan, waarmee Post wordt overschreven om Control.BeginInvoke aan te roepen.
  • WPF biedt DispatcherSynchronizationContext, waarmee Post wordt overschreven om Dispatcher.BeginInvoke aan te roepen.
  • ASP.NET (op .NET Framework) beschikte over een eigen context die ervoor zorgde dat HttpContext.Current beschikbaar was.

Door in plaats van frameworkspecifieke marshaling-API's te gebruiken SynchronizationContext , kunt u onderdelen schrijven die werken in UI-frameworks:

static class SyncContextExample
{
    public static void DoWork()
    {
        // Capture the current SynchronizationContext
        SynchronizationContext? sc = SynchronizationContext.Current;

        ThreadPool.QueueUserWorkItem(_ =>
        {
            // ... do work on the ThreadPool ...

            if (sc is not null)
            {
                sc.Post(_ =>
                {
                    // This runs on the original context (e.g. UI thread)
                    Console.WriteLine("Back on the original context.");
                }, null);
            }
        });
    }
}
Class SyncContextExample
    Public Shared Sub DoWork()
        ' Install a custom SynchronizationContext for demonstration
        Dim customContext As New SimpleSynchronizationContext()
        SynchronizationContext.SetSynchronizationContext(customContext)

        ' Capture the current SynchronizationContext
        Dim sc As SynchronizationContext = SynchronizationContext.Current

        ThreadPool.QueueUserWorkItem(
            Sub(state)
                ' ... do work on the ThreadPool ...

                If sc IsNot Nothing Then
                    sc.Post(
                        Sub(s)
                            ' This runs on the original context (e.g. UI thread)
                            Console.WriteLine("Back on the original context.")
                        End Sub, Nothing)
                Else
                    Console.WriteLine("No SynchronizationContext was captured.")
                End If
            End Sub)
    End Sub
End Class

' A minimal SynchronizationContext for demonstration purposes
Class SimpleSynchronizationContext
    Inherits SynchronizationContext

    Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
        ' Queue the callback to run on a thread pool thread
        ThreadPool.QueueUserWorkItem(
            Sub(s)
                d(state)
            End Sub)
    End Sub
End Class

Een SynchronizationContext vastleggen

Wanneer u een SynchronizationContextopname maakt, leest u de verwijzing uit SynchronizationContext.Current en slaat u deze op voor later gebruik. Vervolgens roept Post u de vastgelegde verwijzing aan om werk terug te plannen naar die omgeving.

Flowing ExecutionContext versus het gebruik van SynchronizationContext

Hoewel beide mechanismen betrekking hebben op het vastleggen van de status van een thread, dienen ze verschillende doeleinden:

  • Flowing ExecutionContext betekent het vastleggen van de omgevingsstatus en het actueel maken van diezelfde status tijdens de uitvoering van een gedelegeerde. De gedelegeerde wordt uitgevoerd waar deze wordt uitgevoerd. De status volgt deze.
  • Het gebruik van SynchronizationContext betekent het vastleggen van een planningsdoel en het gebruiken ervan om te bepalen waar een gedelegeerde wordt uitgevoerd. De vastgelegde context bepaalt waar de gedelegeerde wordt uitgevoerd.

Kortom: ExecutionContext antwoorden 'welke omgeving moet zichtbaar zijn?' terwijl SynchronizationContext antwoorden 'waar moet de code worden uitgevoerd?'

Hoe async/await communiceert met beide contexten

De async/await infrastructuur communiceert automatisch met beide contexten, maar op verschillende manieren.

ExecutionContext volgt altijd

Wanneer een await een methode opschort (omdat de wachter IsCompletedfalse teruggeeft), legt de infrastructuur een ExecutionContext vast. Wanneer de methode wordt hervat, wordt de vervolgbewerking uitgevoerd binnen de vastgelegde context. Dit gedrag is ingebouwd in de opbouwfuncties voor asynchrone methoden (bijvoorbeeld AsyncTaskMethodBuilder) en is van toepassing, ongeacht het type wachtbaar dat u gebruikt.

SuppressFlow() bestaat, maar het is geen await-specifieke switch zoals ConfigureAwait(false). Het voorkomt ExecutionContext vastleggen voor werk dat u in de wachtrij zet zolang de onderdrukking actief is. Het biedt geen optie perawait programmeermodel waarmee de opbouwfuncties voor asynchrone methoden het herstellen van de vastgelegde ExecutionContext methode voor een vervolg moeten overslaan. Dat ontwerp is opzettelijk omdat ExecutionContext ondersteuning op infrastructuurniveau is die thread-lokale semantiek simuleert in een asynchrone wereld, en de meeste ontwikkelaars hoeven er nooit over na te denken.

Taakwachters leggen SynchronizationContext vast

De wachtfuncties voor Task en Task<TResult> bevatten ondersteuning voor SynchronizationContext. De asynchrone methodebouwers bevatten deze ondersteuning niet.

Wanneer u await een taak uitvoert:

  1. De wachter controleert SynchronizationContext.Current.
  2. Als er een context bestaat, legt de wachter deze vast.
  3. Wanneer de taak is voltooid, wordt de voortzetting teruggezet naar die vastgelegde context in plaats van te worden uitgevoerd op de voltooiingsthread of de threadpool.

await "brengt u terug naar waar u was", dat is hoe dit gedrag werkt. Bijvoorbeeld, hervatten van de UI-thread in een bureaubladtoepassing.

ConfigureAwait bepaalt het vastleggen van de SynchronizationContext

Als u het marshalinggedrag niet wilt, roep dan ConfigureAwait aan met false:

await task.ConfigureAwait(false);

Wanneer u continueOnCapturedContext instelt op false, controleert de wachter niet op een SynchronizationContext en wordt de vervolgbewerking uitgevoerd waar de taak is voltooid (meestal op een threadpool-thread). Auteurs van bibliotheken moeten ConfigureAwait(false) op elke await gebruiken, tenzij de code specifiek hervat moet worden in de vastgelegde context.

SynchronizationContext.Current wordt niet doorgegeven bij awaits

Dit punt is het belangrijkste: SynchronizationContext.Currentloopt niet over await-punten. De asynchrone methodebouwers in de runtime maken gebruik van interne overbelastingen die expliciet onderdrukken SynchronizationContext van stromen als onderdeel van ExecutionContext.

Waarom dit belangrijk is

SynchronizationContext Technisch gezien is dit een van de subcontexten die ExecutionContext kunnen bevatten. Als het als onderdeel van ExecutionContext wordt gestroomd, kan code die op een thread pool thread wordt uitgevoerd een gebruikersinterface SynchronizationContext als Current zien, niet omdat die thread de UI-thread is, maar omdat de context via flow is gelekt. Deze wijziging zou de betekenis van SynchronizationContext.Current 'de omgeving waarin ik momenteel ben' veranderen in 'de omgeving die historisch bestond ergens in de oproepketen'.

Het voorbeeld van Task.Run

Houd rekening met code die werk naar de thread-pool verplaatst. Het hier beschreven UI-threadgedrag is alleen van toepassing wanneer SynchronizationContext.Current niet null is, zoals het geval is in een UI-app.

static class TaskRunExample
{
    public static async Task ProcessOnUIThread()
    {
        // This method is called from a thread with a SynchronizationContext.
        // Task.Run offloads work to the thread pool.
        string result = await Task.Run(async () =>
        {
            string data = await DownloadAsync();
            // Compute runs on the thread pool, not the original context,
            // because SynchronizationContext doesn't flow into Task.Run.
            return Compute(data);
        });

        // Back on the original context (the continuation is posted back).
        Console.WriteLine(result);
    }

    private static async Task<string> DownloadAsync()
    {
        await Task.Delay(100);
        return "downloaded data";
    }

    private static string Compute(string data) =>
        $"Computed: {data.Length} chars";
}
Class TaskRunExampleClass
    Public Shared Async Function ProcessOnUIThread() As Task
        ' If a SynchronizationContext is present when this method starts,
        ' the outer await captures it. Task.Run still offloads work to the thread pool.
        Dim result As String = Await Task.Run(
            Async Function()
                Dim data As String = Await DownloadAsync()
                ' Compute runs on the thread pool, not the caller's context,
                ' because SynchronizationContext doesn't flow into Task.Run.
                Return Compute(data)
            End Function)

        ' Resume on the captured context, if one was available.
        Console.WriteLine(result)
    End Function

    Private Shared Async Function DownloadAsync() As Task(Of String)
        Await Task.Delay(100)
        Return "downloaded data"
    End Function

    Private Shared Function Compute(data As String) As String
        Return $"Computed: {data.Length} chars"
    End Function
End Class

In een console-app is SynchronizationContext.Current meestal null, waardoor het codefragment niet wordt hervat op een echte UI-thread. In plaats daarvan illustreert het fragment de regel conceptueel: als een gebruikersinterface SynchronizationContext over await punten stroomt, zou de await binnen de gedelegeerde die aan Task.Run is doorgegeven, die UI-context zien als Current. Het vervolg na await DownloadAsync() wordt vervolgens teruggeplaatst naar de UI-thread, waardoor Compute(data) wordt uitgevoerd op de UI-thread in plaats van op de threadpool. Dat gedrag verslaat het doel van de Task.Run oproep.

Omdat de runtime de SynchronizationContext flow in ExecutionContext onderdrukt, erft de await binnenin Task.Run geen externe UI-context en blijft de voortzetting op de draadpool werken zoals bedoeld.

Overzicht

Aspect Uitvoeringscontext SynchronizationContext
Purpose Draagt de omgevingsstatus over asynchrone grenzen Vertegenwoordigt een doelscheduler (waar de code moet worden uitgevoerd)
Vastgelegd door Opbouwfuncties voor Async-methoden (infrastructuur) Taakwachters (await task)
Stromen in afwachting? Ja, altijd Nee, vastgelegd en gepost naar, niet gestroomd
Suppression API ExecutionContext.SuppressFlow (geavanceerd; zelden nodig) ConfigureAwait(false)
Scope Alle wachtende exemplaren Task en Task<TResult> (aangepaste async-wachters kunnen vergelijkbare logica toevoegen)

Zie ook