ExecutionContext és SynchronizationContext

A async és await használata során két környezettípus játszik fontos, de nagyon különböző szerepet: ExecutionContext és SynchronizationContext. Megtudhatja, hogy mit csinál mindegyik, hogyan lépnek kapcsolatba async/await egymással, és miért nem folynak át a SynchronizationContext.Current a várakozási pontokon.

Mi az a ExecutionContext?

ExecutionContext a környezeti állapot tárolója, amely a program logikai vezérlési folyamatával folyik. A szinkron világban a környezeti információk szálalapú tárolóban (TLS) találhatók, és az adott szálon futó összes kód ezeket az adatokat látja. Az aszinkron világban egy logikai művelet egy szálon indulhat el, felfüggeszthet és folytathat egy másik szálon. A szálhoz kötött adatok nem követik automatikusan,ExecutionContext ezért ezt biztosítja.

A ExecutionContext folyamatai

Rögzítse ExecutionContextExecutionContext.Capture() használatával. A delegált végrehajtása során a ExecutionContext.Run-t használva állítsa vissza azt.

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

A .NET összes aszinkron API-ja, amely elágazást végez—Run, QueueUserWorkItem, BeginRead és mások—rögzíti a ExecutionContext és a tárolt környezetet használja a visszahívások során. Az egyik szál állapotának rögzítése és egy másik szálra való visszaállítása folyamata az, amit a "flowing ExecutionContext" jelent.

Mi az a SynchronizationContext?

SynchronizationContext egy absztrakció, amely egy célkörnyezetet jelöl, ahol a munkát futtatni szeretné. A különböző felhasználói felületi keretrendszerek saját implementációkat biztosítanak:

  • Windows Forms WindowsFormsSynchronizationContext biztosít, amely felülbírálja Post a Control.BeginInvoke hívásához.
  • WPF biztosít DispatcherSynchronizationContext, amely felülbírálja Post annak érdekében, hogy meghívja Dispatcher.BeginInvoke.
  • ASP.NET (.NET keretrendszerben) saját kontextusban biztosította, hogy HttpContext.Current elérhető volt.

SynchronizationContext A keretrendszerspecifikus marshaling API-k helyett olyan összetevőket írhat, amelyek a felhasználói felületi keretrendszerek között működnek:

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

SynchronizationContext rögzítése

Amikor rögzít egy referenciát SynchronizationContext, olvassa el a hivatkozást, SynchronizationContext.Current és tárolja későbbi használatra. Ezután meghívja Post a rögzített hivatkozást, hogy a munka visszaütemezését ütemezze az adott környezetbe.

Folyamatban lévő végrehajtási kontextus vs. a SynchronizationContext használata

Bár mindkét mechanizmus magában foglalja az állapot rögzítését egy szálból, különböző célokat szolgálnak:

  • A ExecutionContext folyamat azt jelenti, hogy rögzíti a környezeti állapotot, és ugyanazt az állapotot aktuálissá teszi a meghatalmazott végrehajtása során. A meghatalmazott mindenhol fut, ahol végül fut – az állapot követi.
  • A SynchronizationContext használata azt jelenti, hogy rögzít egy ütemezési célt, és azt használja annak eldöntésére, hogy a meghatalmazott hol hajtja végre. A rögzített környezet határozza meg, hol fut a delegált.

Röviden: ExecutionContext "milyen környezetnek kell láthatónak lennie?", míg SynchronizationContext a "hol futjon a kód?"

Hogyan kommunikál az aszinkron/várakozás mindkét környezettel?

Az async/await infrastruktúra automatikusan, de különböző módokon kommunikál mindkét környezettel.

ExecutionContext mindig folyik

Amikor egy await felfüggeszt egy függvényt (mert a várakozó objektum IsCompleted visszatér false), az infrastruktúra elkap egy ExecutionContext. Amikor a metódus folytatódik, a folytatás a rögzített környezetben fut. Ez a viselkedés be van építve az aszinkron metóduskészítőkbe (például AsyncTaskMethodBuilder), és függetlenül attól, hogy milyen típusú várakoztathatót használ.

SuppressFlow() létezik, de ez nem egy kifejezetten az 'await'-hez kapcsolódó kapcsoló, mint ConfigureAwait(false). Letiltja ExecutionContext az üzenetsorba helyezett munka rögzítését, miközben a letiltás aktív. Nem biztosít olyan programozási-modellenkéntiawait beállítást, amely arra utasítja az aszinkron metóduskészítőket, hogy hagyják ki a rögzített ExecutionContext visszaállítást a folytatáshoz. Ez a kialakítás szándékos, mert ExecutionContext az infrastruktúraszintű támogatás a szál-helyi szemantikát szimulálja egy aszinkron világban, és a legtöbb fejlesztőnek soha nem kell gondolnia rá.

A feladat várói rögzítik a SynchronizationContextet

A várakozók a Task és Task<TResult> esetében tartalmazzák a SynchronizationContext támogatását. Az aszinkron metóduskészítők nem tartalmazzák ezt a támogatást.

Feladat await esetén:

  1. A váró ellenőrzi SynchronizationContext.Current.
  2. Ha létezik környezet, a váró rögzíti azt.
  3. Amikor a feladat befejeződik, a rendszer a folytatást a rögzített környezetbe helyezi ahelyett, hogy a befejező szálon vagy a szálkészleten futna.

Ez a viselkedés " await visszahozza önt oda, ahol volt". Például a felhasználói felület szálának folytatása egy asztali alkalmazásban.

ConfigureAwait vezérli a SynchronizationContext rögzítését

Ha nem szeretné a marshal viselkedést, hívja a ConfigureAwait-t a következővel: false:

await task.ConfigureAwait(false);

Amikor continueOnCapturedContext be van állítva false, a várakozó nem ellenőrzi SynchronizationContext-t, és a folytatás ott fut, ahol a feladat befejeződik (általában egy szálkészlet szálán). A kódtár-szerzőknek minden várakozáskor használniuk ConfigureAwait(false) kell, kivéve, ha a kódnak kifejezetten a rögzített környezetben kell folytatódnia.

A SynchronizationContext.Current nem halad át a várakozásokon

Ez a pont a legfontosabb: SynchronizationContext.Currentnem halad át a várakozási pontok között. A futtatókörnyezetben az aszinkron metóduskészítők olyan belső túlterheléseket használnak, amelyek kifejezetten megakadályozzák, hogy SynchronizationContext része legyen ExecutionContext.

Miért fontos ez?

Technikailag az egyik alkörnyezet, amelyet a ExecutionContext tartalmazhat, a SynchronizationContext. Ha a folyamat részeként ExecutionContext csatlakozott, a szálkészlet-szálon végrehajtó kód a felhasználói felület kontextusát úgy láthatja SynchronizationContext, mint Current, nem azért, mert ez a szál a felhasználói felület szála, hanem azért, mert a környezet "átáramlása" a folyamaton keresztül történt. Ez a változás megváltoztatná a "környezet, amelyben jelenleg vagyok" jelentését SynchronizationContext.Current a "környezet, amely korábban valahol a hívásláncban létezett".

A Task.Run példa

Gondolja át, hogy a kód kiszervezi-e a feladatokat a szálkötegbe. Az itt ismertetett felhasználói felületi szál viselkedése csak akkor érvényes, ha SynchronizationContext.Current nem null értékű, például egy felhasználói felületi alkalmazásban:

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

A konzolalkalmazásokban általában SynchronizationContext.Currentez történik, null így a kódrészlet nem folytatódik valódi felhasználói felületi szálon. Ehelyett a kódrészlet fogalmilag szemlélteti a szabályt: ha egy felhasználói felület SynchronizationContext pontokat halad át await , az await átadott Task.Run meghatalmazotton belül ez a felhasználói felületi környezet a Currentkövetkezőképpen jelenik meg: . A folytatás ezután await DownloadAsync() visszajuttatódik a felhasználói felületi szálra, aminek eredményeképpen a Compute(data) a felhasználói felületi szálon fut majd, nem pedig a szálkészleten. Ez a viselkedés legyőzi a Task.Run hívás célját.

Mivel a futtatókörnyezet korlátozza a SynchronizationContext vezérlését ExecutionContext-ben, a Task.Run-ben a await nem örökli a külső felhasználói felületi környezetet, és a folytatás a várt módon fut a szálkezelőn.

Összefoglalás

Tulajdonság VégrehajtásiKörnyezet (ExecutionContext) SynchronizationContext
Purpose Az aszinkron határokon át hordozza a környezeti állapotot. Célütemezőt jelöl (ahol a kódnak futnia kell)
Rögzítette: Aszinkron metóduskészítők (infrastruktúra) Feladatra várók (await task)
Folyamatok várnak? Igen, mindig Nem – rögzítve és közzétéve valamire, nem továbbítva.
Letiltási API ExecutionContext.SuppressFlow (speciális; ritkán szükséges) ConfigureAwait(false)
Scope Minden várakoztatható Task és Task<TResult> (az egyéni awaiterek hasonló logikát adhatnak hozzá)

Lásd még