ExecutionContext a SynchronizationContext

Při práci s async a await hrají dva typy kontextu důležité, ale velmi odlišné role: ExecutionContext a SynchronizationContext. Dozvíte se, co každý z nich dělá, jak každý komunikuje async/awaita proč SynchronizationContext.Current neprotéká mezi body await.

Co je ExecutionContext?

ExecutionContext je kontejner pro stav okolí, který plyne s logickým tokem řízení vašeho programu. V synchronním světě se informace o okolí nacházejí v místním úložišti s vlákny (TLS) a veškerý kód spuštěný v daném vlákně vidí tato data. V asynchronním světě může logická operace začínat na jednom vlákně, pozastavit a pokračovat v jiném vlákně. Data místního vlákna nenásledují automaticky – ExecutionContext způsobuje, že se řídí.

Jak toky ExecutionContext

Zachyťte ExecutionContext pomocí ExecutionContext.Capture(). Obnovte během spuštění delegáta pomocí 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

Všechna asynchronní rozhraní API v .NET, která rozdělují práci – Run, QueueUserWorkItem, BeginRead a další – zachycují ExecutionContext a při vyvolání zpětného volání používají uložený kontext. Tento proces zachycení stavu v jednom vlákně a jeho obnovení na jiném je to, co znamená "flowing ExecutionContext".

Co je SynchronizationContext?

SynchronizationContext je abstrakce, která představuje cílové prostředí, ve kterém chcete pracovat. Různé architektury uživatelského rozhraní poskytují vlastní implementace:

  • model Windows Forms poskytuje WindowsFormsSynchronizationContext, který přepisuje Post pro volání Control.BeginInvoke.
  • WPF (Windows Presentation Foundation) poskytuje DispatcherSynchronizationContext, který přepisuje Post a volá Dispatcher.BeginInvoke.
  • ASP.NET (v rozhraní .NET Framework) poskytl vlastní kontext, který zajistil, že byl k dispozici HttpContext.Current.

SynchronizationContext Místo framework-specifických API pro zařazování můžete psát komponenty, které fungují v různých rámcích uživatelského rozhraní:

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

Zachycení synchronizačníhocontextu

Když SynchronizationContext zachytíte, přečtete si odkaz z SynchronizationContext.Current a uložíte ho pro pozdější použití. Potom zavoláte Post na zachyceném odkazu, a naplánujete práci zpět do daného prostředí.

Tok ExecutionContextu vs. použití SynchronizationContextu

I když oba mechanismy zahrnují zachycení stavu z vlákna, slouží různým účelům:

  • Propagace ExecutionContext znamená zachycení okolního stavu a zachování tohoto stavu aktuálním během provádění delegáta. Delegát běží tam, kam se dostane – stav ho následuje.
  • Použití SynchronizationContext znamená zaznamenání plánu cíle a jeho použití k rozhodnutí, kde se delegát provádí. Zachycený kontext řídí, kde delegát běží.

Stručně řečeno: ExecutionContext odpovídá na otázku "jaké prostředí by mělo být viditelné?" zatímco SynchronizationContext odpovídá na otázku "kde by měl kód běžet?"

Jak async/await komunikuje s oběma kontexty

Infrastruktura async/await komunikuje s oběma kontexty automaticky, ale různými způsoby.

ExecutionContext vždy se propaguje

Kdykoli await pozastaví metodu (protože awaiter IsCompleted vrátí false), infrastruktura zachytí ExecutionContext. Když se metoda obnoví, pokračování se spustí v zachyceném kontextu. Toto chování je integrované do tvůrců asynchronních metod (například AsyncTaskMethodBuilder) a platí bez ohledu na to, jaký typ awaitable objektu použijete.

SuppressFlow() existuje, ale nejedná se o přepínač specifický pro await, jako je ConfigureAwait(false). Potlačí zachytávání ExecutionContext pro práci, kterou zařadíte do fronty, zatímco potlačení je aktivní. Neposkytuje možnost podle programovacíhoawait modelu, která dává tvůrcům asynchronních metod pokyn, aby přeskočí obnovení zachyceného ExecutionContext pro pokračování. Tento návrh je záměrný, protože ExecutionContext se jedná o podporu na úrovni infrastruktury, která simuluje sémantiku místních vláken v asynchronním světě a většina vývojářů o tom nemusí přemýšlet.

Funkce Awaiters zachytává SynchronizačníContext

Uživatelé čekající na Task a Task<TResult> zahrnují podporu pro SynchronizationContext. Tvůrci asynchronních metod tuto podporu nezahrnují.

Když await úkol:

  1. Awaiter zkontroluje SynchronizationContext.Current.
  2. Pokud kontext existuje, operátor await ho zachytí.
  3. Po dokončení úlohy se pokračování publikuje zpět do tohoto zachyceného kontextu místo spuštění v dokončeném vlákně nebo fondu vláken.

Toto chování je způsob, jak await vás "vrátí do místa, kde jste byli". Například obnovení vlákna uživatelského rozhraní v desktopové aplikaci.

Konfigurace ConfigureAwait určuje zachycení SynchronizationContext

Pokud nechcete způsob zařazení, zavolejte funkci s ConfigureAwaitfalse:

await task.ConfigureAwait(false);

Když nastavíte continueOnCapturedContext na false, awaiter nekontroluje SynchronizationContext a pokračování pokračuje na libovolném místě, kde se úloha dokončí (obvykle ve vlákně fondu vláken). Autoři knihoven by měli použít ConfigureAwait(false) pro každý await, pokud kód výslovně nepotřebuje obnovit v zachyceném kontextu.

SynchronizationContext.Current se nepřenáší přes operátory await

Tento bod je nejdůležitější: SynchronizationContext.Currentnepřechází přes body await. Tvůrci asynchronních metod v modulu runtime využívají interní přetížení, která explicitně zabraňují SynchronizationContext v přechodu jako součást ExecutionContext.

Proč na tom záleží

Technicky vzato SynchronizationContext je jedním z dílčích kontextů, které ExecutionContext můžou obsahovat. Pokud teklo jako součást ExecutionContext, kód spuštěný na vlákně fondu vláken může vidět uživatelské rozhraní jako SynchronizationContext, ne Current proto, že toto vlákno je vlákno uživatelského rozhraní, ale protože kontext "unikl" prostřednictvím přenosu toku. Tato změna by změnila význam SynchronizationContext.Current z "prostředí, ve které právě jsem" na "prostředí, které v minulosti existovalo někde v řetězci volání".

Příklad Task.Run

Vezměte v úvahu kód, který přesměruje práci do fondu vláken. Chování vlákna uživatelského rozhraní popsané zde platí pouze v případě, že SynchronizationContext.Current není null, například v aplikaci uživatelského rozhraní:

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

V konzolové aplikaci SynchronizationContext.Current je obvykle null, takže fragment kódu nebude pokračovat ve skutečném vlákně uživatelského rozhraní. Místo toho fragment kódu znázorňuje pravidlo koncepčně: pokud uživatelské rozhraní SynchronizationContext proudí přes await body, await, který byl předán delegátovi Task.Run, mohl vnímat kontext uživatelského rozhraní jako Current. Pokračování poté, co await DownloadAsync() odešle zpět do vlákna uživatelského rozhraní, způsobí, že se Compute(data) spustí ve vlákně uživatelského rozhraní, místo ve fondu vláken. Toto chování porazí účel Task.Run volání.

Vzhledem k tomu, že modul runtime potlačuje SynchronizationContext tok v ExecutionContext, await uvnitř Task.Run nedědí vnější kontext uživatelského rozhraní a pokračování běží ve fondu vláken podle plánu.

Shrnutí

Aspekt Kontext provádění SynchronizationContext
Purpose Přenáší okolní stav přes asynchronní hranice. Představuje cílový plánovač (kde by se měl spustit kód).
Zachyceno Asynchronní metoda budování (infrastruktura) Awaiterů úkolů (await task)
Průtoky napříč await? Ano, vždy Ne – zachyceno a zveřejněno, nikoliv přeposláno
API pro potlačení ExecutionContext.SuppressFlow (pokročilé, zřídka potřebné) ConfigureAwait(false)
Scope Všechny čekající úlohy Task a Task<TResult> (vlastní čekací objekty mohou přidat podobnou logiku)

Viz také