ExecutionContext och SynchronizationContext

När du arbetar med async och awaitspelar två kontexttyper viktiga men mycket olika roller: ExecutionContext och SynchronizationContext. Du lär dig vad var och en gör, hur var och en interagerar med async/awaitoch varför SynchronizationContext.Current inte flödar över inväntningspunkter.

Vad är ExecutionContext?

ExecutionContext är en container för omgivande tillstånd som flödar med programmets logiska kontrollflöde. I en synkron värld finns omgivande information i trådlokal lagring (TLS), och all kod som körs på en viss tråd ser dessa data. I en asynkron värld kan en logisk åtgärd starta på en tråd, pausa och återuppta på en annan tråd. Trådlokala data följer inte med automatiskt—ExecutionContext gör att de kan följa med.

Så här flödar ExecutionContext

Avbilda ExecutionContext med hjälp av ExecutionContext.Capture(). Återställ det under utförandet av en delegering genom att använda 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

Alla asynkrona API:er i .NET som förgrenar arbete – Run, QueueUserWorkItem, BeginRead och andra – fångar ExecutionContext och använder den lagrade kontexten när återanropet anropas. Den här processen att överföra state från en tråd till en annan och återställa det är vad som menas med "flowing ExecutionContext".

Vad är SynchronizationContext?

SynchronizationContext är en abstraktion som representerar en målmiljö där du vill att arbetet ska köras. Olika gränssnittsramverk tillhandahåller egna implementeringar:

  • Windows Forms tillhandahåller WindowsFormsSynchronizationContext som åsidosätter Post för att anropa Control.BeginInvoke.
  • WPF tillhandahåller DispatcherSynchronizationContext som åsidosätter Post för att anropa Dispatcher.BeginInvoke.
  • ASP.NET (på .NET Framework) tillhandahöll en egen kontext som säkerställde att HttpContext.Current var tillgänglig.

Genom att använda SynchronizationContext i stället för ramverksspecifika api:er för marskalkering kan du skriva komponenter som fungerar i användargränssnittsramverk:

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

Fånga en synkroniseringskontext

När du registrerar en SynchronizationContextläser du referensen från SynchronizationContext.Current och lagrar den för senare användning. Sedan anropar Post du den insamlade referensen för att schemalägga arbete tillbaka till den miljön.

Flödande ExecutionContext jämfört med synkroniseringskontext

Även om båda mekanismerna omfattar att samla in tillstånd från en tråd, tjänar de olika syften:

  • Flowing ExecutionContext innebär att fånga det omgivande tillståndet och göra detta tillstånd aktuellt under körningen av en delegat. Delegeringen körs var den än hamnar – tillståndet följer med.
  • Att använda SynchronizationContext innebär att fånga ett schemaläggningsmål och använda det för att bestämma var en delegering körs. Den insamlade kontexten styr var delegeringen körs.

Kort och kort: ExecutionContext svarar "vilken miljö ska vara synlig?" medan SynchronizationContext svar "var ska koden köras?"

Hur async/await interagerar med båda kontexterna

Infrastrukturen async/await interagerar automatiskt med båda kontexterna, men på olika sätt.

ExecutionContext flödar alltid

När en await pausar en metod (eftersom väntarens IsCompleted returnerar false), fångar infrastrukturen en ExecutionContext. När metoden återupptas körs fortsättningen inom den inspelade kontexten. Det här beteendet är inbyggt i asynkrona metodbyggare (till exempel AsyncTaskMethodBuilder) och gäller oavsett vilken typ av väntbar du använder.

SuppressFlow() finns, men det är inte en inväntningsspecifik växel som ConfigureAwait(false). Den förhindrar ExecutionContext inspelning för det arbete som du köar medan undertryckning är aktiv. Det tillhandahåller inte ett alternativ perawait programmeringsmodell som talar om för async-metodskaparna att hoppa över återställningen av den insamlade ExecutionContext filen för en fortsättning. Den designen är avsiktlig eftersom ExecutionContext är stöd på infrastrukturnivå som simulerar trådlokal semantik i en asynkron värld, och de flesta utvecklare behöver aldrig tänka på den.

Väntare för uppgifter fångar synkroniseringskontext

Awaiters för Task och Task<TResult> inkluderar stöd för SynchronizationContext. Asynkrona metodbyggare inkluderar inte det här stödet.

När du await har en uppgift:

  1. väntaren kontrollerar SynchronizationContext.Current.
  2. Om en kontext finns, fångar inväntaren den.
  3. När uppgiften är klar publiceras fortsättningen tillbaka till den insamlade kontexten i stället för att köras på den tråd som slutförde uppgiften eller i trådpoolen.

Så här beter sig await och "tar dig tillbaka till där du var". Du kan till exempel återuppta användargränssnittstråden i ett skrivbordsprogram.

ConfigureAwait kontrollerar fångst av SynchronizationContext

Om du inte vill ha marskalkningsbeteendet anropar du ConfigureAwait med false:

await task.ConfigureAwait(false);

När du anger continueOnCapturedContext till falsesöker inte inläsaren efter en SynchronizationContext och fortsättningen körs där uppgiften slutförs (vanligtvis i en trådpoolstråd). Biblioteksförfattare bör använda ConfigureAwait(false) vid varje inväntning om inte koden specifikt behöver återupptas i den insamlade kontexten.

SynchronizationContext.Current flödar inte över await uttryck

Den här punkten är den viktigaste: SynchronizationContext.Currentflödar inte över inväntningspunkter. Asynkrona metodbyggare i körningsmiljön använder interna överbelastningar som uttryckligen undertrycker SynchronizationContext från flöde som en del av ExecutionContext.

Varför detta är viktigt

Tekniskt sett är SynchronizationContext en av de underkontexter som ExecutionContext kan innehålla. Om det flödade som en del av ExecutionContext, kan kod som körs på en trådpoolstråd se ett användargränssnitt SynchronizationContext som Current, inte för att den tråden är användargränssnittstråden, utan för att kontexten "läckte" via flödet. Den ändringen skulle ändra innebörden av SynchronizationContext.Current från "miljön jag för närvarande är i" till "miljön som historiskt funnits någonstans i samtalskedjan".

Exemplet “Task.Run”

Överväg kod som avlastar arbete till trådpoolen. Beteendet för användargränssnittstråden som beskrivs här gäller endast när SynchronizationContext.Current inte är icke-null, till exempel i en 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

I en konsolapp SynchronizationContext.Current är det vanligtvis null, så kodfragmentet återupptas inte på en riktig UI-tråd. I stället illustrerar kodfragmentet konceptuellt regeln: om ett användargränssnitt SynchronizationContext flödade över await punkter, skulle await i delegeringen som skickades till Task.Run se den användargränssnittskontext som Current. Fortsättningen efter await DownloadAsync() skulle sedan skickas tillbaka till användargränssnittstråden, vilket innebär att Compute(data) körs på användargränssnittstråden i stället för på trådpoolen. Det beteendet motverkar syftet med Task.Run samtalet.

Eftersom körningsmiljön undertrycker SynchronizationContext flödet i ExecutionContext, ärver await inuti Task.Run inte en extern UI-kontext, och fortsättningen fortsätter att köras på trådpoolen som avsett.

Sammanfattning

Aspekt Exekveringskontext SynkroniseringContext
Purpose Bär miljötillstånd över asynkrona gränser Representerar en målschemaläggare (där koden kommer att köras)
Fångad av Asynkrona metodbyggare (infrastruktur) Uppgiftsväntare (await task)
Flöden över väntar? Ja, alltid Nej – insamlad och postad till, inte flödad
Suppression API ExecutionContext.SuppressFlow (avancerat; behövs sällan) ConfigureAwait(false)
Scope Alla "awaitables" Task och Task<TResult> (anpassade awaiters kan lägga till liknande logik)

Se även