ExecutionContext dan SynchronizationContext

Ketika Anda bekerja dengan async dan await, dua jenis konteks memainkan peran penting tetapi sangat berbeda: ExecutionContext dan SynchronizationContext. Anda mempelajari apa yang dilakukan masing-masing, bagaimana masing-masing berinteraksi dengan async/await, dan mengapa SynchronizationContext.Current tidak mengalir di titik tunggu.

Apa itu ExecutionContext?

ExecutionContext adalah kontainer untuk keadaan ambien yang mengikuti alur kontrol logis program Anda. Dalam dunia yang sinkron, informasi sekitar berada di penyimpanan lokal utas (TLS), dan semua kode yang berjalan pada utas tertentu melihat data tersebut. Di dunia asinkron, operasi logis dapat dimulai pada satu utas, menangguhkan, dan melanjutkan pada utas yang berbeda. Data thread-local tidak mengikuti secara otomatis—ExecutionContext membuatnya mengikuti.

Cara kerja aliran ExecutionContext

Tangkap ExecutionContext dengan menggunakan ExecutionContext.Capture(). Kembalikan selama eksekusi delegate dengan menggunakan 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

Semua API asinkron dalam .NET yang mencabangkan pekerjaan—Run, QueueUserWorkItem, BeginRead, dan lainnya—menangkap ExecutionContext dan menggunakan konteks tersimpan saat memanggil callback Anda. Proses menangkap status pada satu utas dan memulihkannya di utas lain inilah yang berarti "flowing ExecutionContext".

Apa itu SynchronizationContext?

SynchronizationContext adalah abstraksi yang mewakili lingkungan target tempat Anda ingin bekerja. Kerangka kerja UI yang berbeda menyediakan implementasinya sendiri:

  • Formulir Windows menyediakan WindowsFormsSynchronizationContext, yang menggantikan Post untuk memanggil Control.BeginInvoke.
  • WPF menyediakan DispatcherSynchronizationContext, yang mengambil alih Post untuk memanggil Dispatcher.BeginInvoke.
  • ASP.NET (pada .NET Framework) menyediakan konteksnya sendiri yang memastikan HttpContext.Current tersedia.

Dengan menggunakan SynchronizationContext alih-alih API marshaling khusus kerangka kerja, Anda dapat menulis komponen yang berfungsi di berbagai kerangka kerja UI.

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

Menangkap SynchronizationContext

Saat Anda mengambil SynchronizationContext, Anda membaca referensi dari SynchronizationContext.Current dan menyimpannya untuk digunakan nanti. Anda kemudian memanggil referensi Post yang telah diambil untuk menjadwalkan pekerjaan kembali ke lingkungan tersebut.

Pengaliran ExecutionContext vs. menggunakan SynchronizationContext

Meskipun kedua mekanisme melibatkan pengambilan status dari utas, kedua mekanisme tersebut melayani tujuan yang berbeda:

  • Flowing ExecutionContext berarti menangkap keadaan lingkungan dan menerapkan keadaan yang sama selama eksekusi delegat. Delegasi berjalan di mana pun akhirnya berjalan—status mengikutinya.
  • Menggunakan SynchronizationContext berarti menangkap target penjadwalan dan menggunakannya untuk memutuskan lokasi eksekusi delegasi. Konteks yang diambil mengontrol tempat delegasi berjalan.

Singkatnya: ExecutionContext menjawab "lingkungan apa yang harus terlihat?" sambil SynchronizationContext menjawab "di mana kode harus dijalankan?"

Cara asinkron/menunggu berinteraksi dengan kedua konteks

Infrastruktur async/await berinteraksi dengan kedua konteks secara otomatis, tetapi dengan cara yang berbeda.

ExecutionContext selalu mengalir

Setiap kali sebuah await menangguhkan metode (karena properti awaiter IsCompleted mengembalikan false), infrastruktur menangkap sebuah ExecutionContext. Ketika metode dilanjutkan, kelanjutan berjalan dalam konteks yang ditangkap. Perilaku ini sudah terintegrasi dalam penyusun metode asinkron (misalnya, AsyncTaskMethodBuilder) dan berlaku terlepas dari jenis awaitable apa pun yang Anda gunakan.

SuppressFlow() ada, tetapi bukan pengubah khusus 'await' seperti ConfigureAwait(false). Ini menghentikan ExecutionContext pengambilan untuk pekerjaan yang Anda mengantre saat supresi aktif. Ini tidak menyediakan opsi untuk setiap modelawait pemrograman yang menginstruksikan pembangun metode asinkron untuk melewati memulihkan ExecutionContext yang telah diambil untuk kelanjutan. Desain itu disengaja karena ExecutionContext merupakan dukungan tingkat infrastruktur yang mensimulasikan semantik utas lokal dalam lingkungan asinkron, dan sebagian besar pengembang tidak perlu memikirkannya.

Penanti tugas menangkap SynchronizationContext

Penanti dari Task dan Task<TResult> juga mendukung SynchronizationContext. Penyusun metode asinkron tidak menyertakan dukungan ini.

Saat Anda await melakukan tugas:

  1. Penunggu memeriksa SynchronizationContext.Current.
  2. Jika terdapat konteks, pengawas menanganinya.
  3. Ketika tugas selesai, kelanjutan diposting kembali ke konteks yang diambil alih-alih berjalan pada utas yang selesai atau kumpulan utas.

Perilaku ini adalah bagaimana await "membawa Anda kembali ke tempat Anda berada". Misalnya, memulai kembali utas UI di aplikasi desktop.

Kontrol ConfigureAwait menangkap SynchronizationContext

Jika Anda tidak ingin perilaku pemetaan, panggil ConfigureAwait dengan false:

await task.ConfigureAwait(false);

Saat Anda mengatur continueOnCapturedContext ke false, awaiter tidak memeriksa SynchronizationContext dan kelanjutan dieksekusi di mana pun tugas selesai (biasanya pada pool thread). Penulis pustaka harus menggunakan ConfigureAwait(false) pada setiap menunggu kecuali kode secara khusus perlu dilanjutkan pada konteks yang diambil.

SynchronizationContext.Current tidak diteruskan melalui await

Titik ini adalah yang paling penting: SynchronizationContext.Currenttidak bergerak pada titik penundaan. Pembangun metode async dalam runtime menggunakan overload internal yang secara eksplisit menekan SynchronizationContext agar tidak mengalir sebagai bagian dari ExecutionContext.

Mengapa ini penting

Secara teknis, SynchronizationContext adalah salah satu sub-konteks yang ExecutionContext dapat berisi. Jika mengalir sebagai bagian dari ExecutionContext, kode yang dijalankan pada utas kumpulan mungkin melihat UI SynchronizationContext sebagai Current, bukan karena utas tersebut adalah utas UI, tetapi karena konteks "bocor" melalui alur. Perubahan itu akan mengubah arti SynchronizationContext.Current dari "lingkungan yang saat ini saya gunakan" menjadi "lingkungan yang secara historis ada di suatu tempat dalam rantai panggilan."

Contoh Task.Run

Pertimbangkan kode yang memindahkan pekerjaan ke kumpulan utas. Perilaku UI-thread yang dijelaskan di sini hanya berlaku saat SynchronizationContext.Current non-null, seperti di aplikasi UI:

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

Di aplikasi konsol, SynchronizationContext.Current biasanya null, sehingga cuplikan tidak dilanjutkan pada utas UI nyata. Sebaliknya, cuplikan ini mengilustrasikan aturan secara konseptual: jika UI SynchronizationContext mengalir melintasi await titik, maka await di dalam delegasi yang diteruskan ke Task.Run akan melihat konteks UI tersebut sebagai Current. Kelanjutan setelah await DownloadAsync() kemudian akan mengirim kembali ke utas UI, menyebabkan Compute(data) dijalankan pada utas UI alih-alih pada kumpulan utas. Perilaku itu mengalahkan tujuan panggilan Task.Run.

Karena runtime menghentikan aliran di ExecutionContext, elemen await dalam Task.Run tidak mewarisi konteks UI luar, dan proses lanjutan tetap berjalan di kumpulan utas sesuai yang direncanakan.

RINGKASAN

Aspek ExecutionContext Konteks Sinkronisasi
Purpose Membawa keadaan lingkungan di seluruh batas proses asinkron Mewakili penjadwal target (di mana kode harus berjalan)
Diambil oleh Pembangun metode asinkron (infrastruktur) Penanti tugas (await task)
Apakah aliran berhenti saat menunggu? Ya, selalu Tidak—diambil dan diunggah ke, tidak ditransfer
API Pembatalan ExecutionContext.SuppressFlow (tingkat lanjut; jarang diperlukan) ConfigureAwait(false)
Cakupan Semua yang dapat ditunggu Task dan Task<TResult> (awaiter kustom dapat menambahkan logika serupa)

Baca juga