System Programming Windows

SynchronizationContext та ConfigureAwait — Контекст Виконання

Глибокий академічний розбір SynchronizationContext у .NET — UI thread affinity, ConfigureAwait(false), deadlock scenarios, ExecutionContext, AsyncLocal<T> та best practices для бібліотечного та UI коду.

SynchronizationContext та ConfigureAwait — Контекст Виконання

Вступ: Проблема Контексту Виконання

У попередній темі ви навчились писати async/await код і розумієте що await звільняє потік під час очікування I/O операції. Але є критично важливе питання, яке ми поки що оминули: на якому потоці продовжується виконання після await?

Розгляньмо простий приклад:

ThreadAfterAwait.cs
async Task ExampleAsync()
{
    Console.WriteLine($"До await: потік {Thread.CurrentThread.ManagedThreadId}");
    
    await Task.Delay(1000);
    
    Console.WriteLine($"Після await: потік {Thread.CurrentThread.ManagedThreadId}");
}

Запустіть цей код у консольному додатку:

До await: потік 1
Після await: потік 4

Продовження виконалось на іншому потоці (ThreadPool). Це нормально для консольних додатків.

Тепер запустіть той самий код у WPF/WinForms додатку:

До await: потік 1 (UI thread)
Після await: потік 1 (UI thread)

Продовження виконалось на тому самому потоці (UI thread)! Чому?

Відповідь: SynchronizationContext. Це механізм який контролює де виконується continuation після await. Розуміння цього механізму критично важливе для:

  • Написання UI додатків (WPF, WinForms, MAUI)
  • Уникнення deadlock-ів
  • Написання ефективного бібліотечного коду
  • Розуміння різниці між ASP.NET Framework та ASP.NET Core

1. SynchronizationContext: Що Це?

Визначення та Призначення

SynchronizationContext — це абстракція яка представляє контекст виконання коду. Це клас який дозволяє планувати виконання делегатів у специфічному контексті (наприклад, на UI потоці).

public class SynchronizationContext
{
    // Отримати поточний контекст
    public static SynchronizationContext? Current { get; }
    
    // Виконати делегат у цьому контексті (асинхронно)
    public virtual void Post(SendOrPostCallback d, object? state);
    
    // Виконати делегат у цьому контексті (синхронно)
    public virtual void Send(SendOrPostCallback d, object? state);
}

Ключові методи:

  • Current — повертає SynchronizationContext поточного потоку (або null)
  • Post() — виконати делегат асинхронно у цьому контексті
  • Send() — виконати делегат синхронно у цьому контексті (блокує поточний потік)

Навіщо Потрібен SynchronizationContext?

Проблема яку він вирішує: UI frameworks мають thread affinity — UI елементи можна змінювати тільки з UI потоку.

UIThreadAffinity.cs
// WPF додаток
private async void Button_Click(object sender, RoutedEventArgs e)
{
    // Цей код виконується на UI потоці
    StatusLabel.Text = "Завантаження...";
    
    // Завантажуємо дані (I/O-bound)
    string data = await DownloadDataAsync();
    
    // ❓ На якому потоці ми тепер? Чи можемо змінити UI?
    StatusLabel.Text = $"Завантажено: {data.Length} символів";
}

Без SynchronizationContext: продовження після await виконається на ThreadPool потоці, і спроба змінити StatusLabel.Text кине InvalidOperationException (cross-thread operation).

З SynchronizationContext: продовження автоматично повертається на UI потік, і зміна StatusLabel.Text працює коректно.

Як Працює Захоплення Контексту

Коли ви викликаєте await, компілятор генерує код який:

  1. Захоплює (captures) поточний SynchronizationContext.Current
  2. Коли операція завершується — планує (schedules) continuation через захоплений контекст
// Спрощена концептуальна модель (не реальний код)
var capturedContext = SynchronizationContext.Current;

awaiter.OnCompleted(() =>
{
    if (capturedContext != null)
    {
        // Виконати continuation у захопленому контексті
        capturedContext.Post(_ => ContinuationCode(), null);
    }
    else
    {
        // Немає контексту — виконати на ThreadPool
        ThreadPool.QueueUserWorkItem(_ => ContinuationCode());
    }
});

Типи SynchronizationContext

Різні frameworks надають різні реалізації SynchronizationContext:

FrameworkSynchronizationContextПоведінка
Console AppnullContinuation на ThreadPool
WPFDispatcherSynchronizationContextContinuation на UI thread (Dispatcher)
WinFormsWindowsFormsSynchronizationContextContinuation на UI thread (message loop)
MAUIMauiSynchronizationContextContinuation на UI thread
ASP.NET FrameworkAspNetSynchronizationContextContinuation на request thread
ASP.NET CorenullContinuation на ThreadPool (немає контексту!)
xUnit/NUnitCustom або nullЗалежить від test runner
Ключова різниця: ASP.NET Core не маєSynchronizationContext — це одна з причин його високої продуктивності. Continuation виконується на будь-якому ThreadPool потоці.

Приклад: Ручне Використання SynchronizationContext

ManualSyncContext.cs
// Захопити поточний контекст
SynchronizationContext? context = SynchronizationContext.Current;

// Виконати роботу на ThreadPool
ThreadPool.QueueUserWorkItem(_ =>
{
    // Виконуємо CPU-bound роботу
    string result = PerformHeavyComputation();
    
    // Повернутись у захоплений контекст (наприклад, UI thread)
    if (context != null)
    {
        context.Post(_ =>
        {
            // Цей код виконається на UI потоці
            UpdateUI(result);
        }, null);
    }
});

Це те, що async/await робить автоматично!

Візуалізація: Консольний Додаток vs UI Додаток

Консольний додаток (немає SynchronizationContext):

Thread 1 (Main):
  ↓
  await Task.Delay(1000)
  ↓
  SynchronizationContext.Current = null
  ↓
  Continuation планується на ThreadPool
  ↓
Thread 4 (ThreadPool):
  ↓
  Continuation виконується

WPF додаток (є DispatcherSynchronizationContext):

Thread 1 (UI):
  ↓
  await Task.Delay(1000)
  ↓
  SynchronizationContext.Current = DispatcherSynchronizationContext
  ↓
  Continuation планується через Dispatcher
  ↓
Thread 1 (UI):
  ↓
  Continuation виконується на UI потоці

Перевірка Поточного Контексту

CheckSyncContext.cs
void PrintCurrentContext()
{
    var context = SynchronizationContext.Current;
    
    if (context == null)
    {
        Console.WriteLine("Немає SynchronizationContext (консольний додаток або ASP.NET Core)");
    }
    else
    {
        Console.WriteLine($"SynchronizationContext: {context.GetType().Name}");
    }
    
    Console.WriteLine($"Потік: {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"ThreadPool потік: {Thread.CurrentThread.IsThreadPoolThread}");
}

// У консольному додатку
PrintCurrentContext();
// Вивід: Немає SynchronizationContext, Потік: 1, ThreadPool: False

// У WPF додатку (на UI потоці)
PrintCurrentContext();
// Вивід: SynchronizationContext: DispatcherSynchronizationContext, Потік: 1, ThreadPool: False

Чому Це Важливо?

Розуміння SynchronizationContext критично важливе для:

1. UI Додатків — продовження повинно виконуватись на UI потоці для зміни UI елементів

2. Deadlock-ів — неправильне використання .Result або .Wait() у UI коді призводить до deadlock (детально в розділі 4)

3. Performance — захоплення та відновлення контексту має overhead. У бібліотечному коді використовуйте ConfigureAwait(false) (розділ 3)

4. ASP.NET Core — немає контексту, тому немає overhead та deadlock-ів


2. UI SynchronizationContext: WPF та WinForms

Тепер детально розглянемо як працює SynchronizationContext у UI додатках. Це найпоширеніший use case для розуміння цього механізму.

WPF: DispatcherSynchronizationContext

У WPF всі UI операції повинні виконуватись на UI потоці через Dispatcher. DispatcherSynchronizationContext — це обгортка над Dispatcher яка інтегрується з async/await.

WPF_Example.cs
// WPF Window
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    
    private async void DownloadButton_Click(object sender, RoutedEventArgs e)
    {
        // [1] Виконується на UI потоці
        Console.WriteLine($"[1] Потік: {Thread.CurrentThread.ManagedThreadId}, UI: {!Thread.CurrentThread.IsThreadPoolThread}");
        
        StatusLabel.Content = "Завантаження...";
        DownloadButton.IsEnabled = false;
        
        // [2] await — захоплює DispatcherSynchronizationContext
        string data = await DownloadDataAsync("https://example.com");
        
        // [3] Continuation автоматично повертається на UI потік!
        Console.WriteLine($"[3] Потік: {Thread.CurrentThread.ManagedThreadId}, UI: {!Thread.CurrentThread.IsThreadPoolThread}");
        
        // Можемо безпечно змінювати UI
        StatusLabel.Content = $"Завантажено: {data.Length} символів";
        DownloadButton.IsEnabled = true;
    }
    
    async Task<string> DownloadDataAsync(string url)
    {
        using var client = new HttpClient();
        return await client.GetStringAsync(url);
    }
}
WPF Output
[1] Потік: 1, UI: True
[3] Потік: 1, UI: True
Continuation виконався на тому самому UI потоці!

Що відбувається під капотом:

  1. await захоплює DispatcherSynchronizationContext.Current
  2. Коли GetStringAsync завершується — continuation планується через Dispatcher.BeginInvoke()
  3. Dispatcher виконує continuation на UI потоці

WinForms: WindowsFormsSynchronizationContext

У WinForms механізм аналогічний, але використовує Control.Invoke() замість Dispatcher:

WinForms_Example.cs
// WinForms Form
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }
    
    private async void downloadButton_Click(object sender, EventArgs e)
    {
        // [1] Виконується на UI потоці
        statusLabel.Text = "Завантаження...";
        downloadButton.Enabled = false;
        
        // [2] await — захоплює WindowsFormsSynchronizationContext
        string data = await DownloadDataAsync("https://example.com");
        
        // [3] Continuation автоматично повертається на UI потік
        statusLabel.Text = $"Завантажено: {data.Length} символів";
        downloadButton.Enabled = true;
    }
    
    async Task<string> DownloadDataAsync(string url)
    {
        using var client = new HttpClient();
        return await client.GetStringAsync(url);
    }
}

Проблема: Cross-Thread Operations

Без SynchronizationContext спроба змінити UI з іншого потоку кине exception:

CrossThreadError.cs
// ❌ Неправильно — зміна UI з ThreadPool потоку
private void BadButton_Click(object sender, EventArgs e)
{
    Task.Run(() =>
    {
        Thread.Sleep(1000);
        
        // InvalidOperationException: Cross-thread operation not valid
        statusLabel.Text = "Готово";  // CRASH!
    });
}

// ✅ Правильно — async/await автоматично повертає на UI потік
private async void GoodButton_Click(object sender, EventArgs e)
{
    await Task.Delay(1000);
    
    // Це працює — ми на UI потоці
    statusLabel.Text = "Готово";
}

Ручне Повернення на UI Потік

Якщо з якоїсь причини ви не можете використовувати async/await, можна вручну повернутись на UI потік:

ManualUIReturn.cs
// WPF
private void Button_Click(object sender, RoutedEventArgs e)
{
    Task.Run(() =>
    {
        // Виконуємо роботу на ThreadPool
        string result = PerformHeavyComputation();
        
        // Повертаємось на UI потік
        Dispatcher.Invoke(() =>
        {
            StatusLabel.Content = result;
        });
    });
}

// WinForms
private void button_Click(object sender, EventArgs e)
{
    Task.Run(() =>
    {
        // Виконуємо роботу на ThreadPool
        string result = PerformHeavyComputation();
        
        // Повертаємось на UI потік
        this.Invoke(new Action(() =>
        {
            statusLabel.Text = result;
        }));
    });
}

Але async/await робить це автоматично і елегантніше!


3. ConfigureAwait(false): Відмова від Контексту

Тепер розглянемо найважливіший метод для написання ефективного бібліотечного коду: ConfigureAwait(false).

Проблема: Overhead Захоплення Контексту

Захоплення та відновлення SynchronizationContext має overhead:

  1. Захопити SynchronizationContext.Current (heap allocation якщо не null)
  2. Зареєструвати continuation через context.Post()
  3. Dispatcher/message loop обробляє continuation
  4. Context switch на UI потік

Для бібліотечного коду який не потребує UI потоку — це марний overhead.

ConfigureAwait(false) — Що Це?

ConfigureAwait(false) каже компілятору: не захоплюй SynchronizationContext, виконай continuation на ThreadPool.

ConfigureAwaitFalse.cs
async Task<string> LibraryMethodAsync(string url)
{
    using var client = new HttpClient();
    
    // ConfigureAwait(false) — не захоплювати контекст
    string content = await client.GetStringAsync(url).ConfigureAwait(false);
    
    // Continuation виконається на ThreadPool (не на UI потоці)
    return content.ToUpper();
}

Що відбувається:

  • await не захоплює SynchronizationContext.Current
  • Continuation виконується на ThreadPool потоці
  • Немає overhead повернення на UI потік

Коли Використовувати ConfigureAwait(false)?

✅ Завжди у бібліотечному коді:

// Бібліотечний метод — не потребує UI потоку
public async Task<User> GetUserAsync(int userId)
{
    var response = await httpClient.GetAsync($"/users/{userId}").ConfigureAwait(false);
    var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    return JsonSerializer.Deserialize<User>(json);
}

❌ Ніколи у UI коді:

// UI код — потребує UI потік для зміни UI
private async void Button_Click(object sender, EventArgs e)
{
    // ❌ НЕ використовуйте ConfigureAwait(false) тут!
    string data = await DownloadDataAsync().ConfigureAwait(false);
    
    // CRASH! Ми на ThreadPool потоці, не можемо змінити UI
    StatusLabel.Text = data;
}

Правило: ConfigureAwait(false) Всюди у Бібліотеці

У бібліотечному коді додавайте ConfigureAwait(false) до кожного await:

LibraryConfigureAwait.cs
public async Task<ProcessedData> ProcessAsync(string input)
{
    // Крок 1: завантажити дані
    var rawData = await DownloadAsync(input).ConfigureAwait(false);
    
    // Крок 2: трансформувати
    var transformed = await TransformAsync(rawData).ConfigureAwait(false);
    
    // Крок 3: валідувати
    await ValidateAsync(transformed).ConfigureAwait(false);
    
    // Крок 4: зберегти
    await SaveAsync(transformed).ConfigureAwait(false);
    
    return transformed;
}
Чому кожен await?Після першого ConfigureAwait(false) ви вже на ThreadPool потоці, але наступні await можуть захопити контекст якщо хтось встановить його вручну. Безпечніше додавати ConfigureAwait(false) всюди.

ConfigureAwait(true) — За Замовчуванням

ConfigureAwait(true) — це поведінка за замовчуванням (захоплювати контекст). Явно писати не потрібно:

// Ці два рядки еквівалентні
await SomeMethodAsync();
await SomeMethodAsync().ConfigureAwait(true);

Performance Benchmark

ConfigureAwaitBenchmark.cs
// Benchmark у WPF додатку
async Task<int> WithContextAsync()
{
    int sum = 0;
    for (int i = 0; i < 1000; i++)
    {
        await Task.Yield();  // захоплює контекст
        sum += i;
    }
    return sum;
}

async Task<int> WithoutContextAsync()
{
    int sum = 0;
    for (int i = 0; i < 1000; i++)
    {
        await Task.Yield().ConfigureAwait(false);  // не захоплює
        sum += i;
    }
    return sum;
}

Результати (у WPF додатку):

WithContextAsync:    847ms  (1000 context switches на UI потік)
WithoutContextAsync: 12ms   (виконується на ThreadPool)

Прискорення: 70x

ASP.NET Core: ConfigureAwait Не Потрібен

У ASP.NET Core немає SynchronizationContext, тому ConfigureAwait(false) не дає переваг:

// ASP.NET Core Controller
public async Task<IActionResult> GetUser(int id)
{
    // ConfigureAwait(false) тут марний — немає контексту
    var user = await userService.GetUserAsync(id).ConfigureAwait(false);
    return Ok(user);
}

Але додавати його все одно безпечно і рекомендовано для сумісності з іншими frameworks.


4. Deadlock Scenario: Класична Пастка

Тепер розглянемо найнебезпечнішу пастку async/await — deadlock через неправильне використання .Result або .Wait() у коді з SynchronizationContext.

Класичний Deadlock: UI Thread + .Result

ClassicDeadlock.cs
// WPF/WinForms додаток
private void Button_Click(object sender, EventArgs e)
{
    // ❌ DEADLOCK!
    string result = DownloadDataAsync().Result;
    StatusLabel.Text = result;
}

async Task<string> DownloadDataAsync()
{
    using var client = new HttpClient();
    
    // await захоплює UI SynchronizationContext
    string content = await client.GetStringAsync("https://example.com");
    
    return content;
}

Що відбувається (крок за кроком):

1. UI Thread викликає DownloadDataAsync().Result
   ↓
2. .Result блокує UI Thread і чекає завершення Task
   ↓
3. DownloadDataAsync() починає виконуватись на UI Thread
   ↓
4. await client.GetStringAsync() захоплює DispatcherSynchronizationContext
   ↓
5. HTTP запит відправлено, UI Thread звільняється... НІ! Він БЛОКОВАНИЙ на .Result!
   ↓
6. HTTP запит завершується, continuation планується через Dispatcher
   ↓
7. Dispatcher намагається виконати continuation на UI Thread
   ↓
8. Але UI Thread БЛОКОВАНИЙ на .Result і чекає завершення Task
   ↓
9. Task чекає UI Thread для continuation
   ↓
10. UI Thread чекає Task для .Result
   ↓
   DEADLOCK! 🔒

Візуалізація deadlock:

UI Thread:                          Task:
  |                                   |
  | .Result (блокує)                  |
  |------------------------→ чекає завершення
  |                                   |
  | БЛОКОВАНИЙ                        | await (захопив UI context)
  |                                   |
  | ← чекає UI Thread                 | continuation готовий
  |   для continuation                |
  |                                   |
  🔒 DEADLOCK 🔒

Чому Це Deadlock?

Ключові умови для deadlock:

  1. UI Thread блокується на .Result або .Wait()
  2. Async метод захоплює SynchronizationContext (UI context)
  3. Continuation потребує UI Thread для виконання
  4. UI Thread зайнятий очікуванням Task

Результат: циклічна залежність → deadlock.

Рішення 1: Async All The Way

// ✅ Правильно — async all the way
private async void Button_Click(object sender, EventArgs e)
{
    string result = await DownloadDataAsync();  // не блокує UI Thread
    StatusLabel.Text = result;
}

Рішення 2: ConfigureAwait(false) у Бібліотеці

async Task<string> DownloadDataAsync()
{
    using var client = new HttpClient();
    
    // ConfigureAwait(false) — не захоплювати контекст
    string content = await client.GetStringAsync("https://example.com").ConfigureAwait(false);
    
    return content;
}

// Тепер .Result не призведе до deadlock (але все одно не рекомендовано!)
private void Button_Click(object sender, EventArgs e)
{
    string result = DownloadDataAsync().Result;  // працює, але блокує UI
    StatusLabel.Text = result;
}

ASP.NET Framework Deadlock

Той самий deadlock може статись у ASP.NET Framework (не Core!):

// ASP.NET Framework Controller (старий ASP.NET)
public ActionResult Index()
{
    // ❌ DEADLOCK!
    var data = GetDataAsync().Result;
    return View(data);
}

async Task<Data> GetDataAsync()
{
    // await захоплює AspNetSynchronizationContext
    var response = await httpClient.GetAsync("https://api.example.com");
    return await response.Content.ReadAsAsync<Data>();
}

Чому у ASP.NET Core немає цієї проблеми?

ASP.NET Core не має SynchronizationContext, тому continuation виконується на будь-якому ThreadPool потоці. Deadlock неможливий!

Практичний Приклад: Deadlock Demo

DeadlockDemo.cs
// WPF Window
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    
    // ❌ Deadlock button
    private void DeadlockButton_Click(object sender, RoutedEventArgs e)
    {
        StatusLabel.Content = "Починаємо deadlock...";
        
        // Це зависне назавжди
        string result = SlowOperationAsync().Result;
        
        StatusLabel.Content = result;  // ніколи не виконається
    }
    
    // ✅ Правильний button
    private async void CorrectButton_Click(object sender, RoutedEventArgs e)
    {
        StatusLabel.Content = "Починаємо правильну операцію...";
        
        string result = await SlowOperationAsync();
        
        StatusLabel.Content = result;  // виконається коректно
    }
    
    async Task<string> SlowOperationAsync()
    {
        await Task.Delay(2000);  // захоплює UI context
        return "Готово!";
    }
}

Як Виявити Deadlock?

Симптоми:

  • UI додаток "завис" (не реагує на кліки)
  • Debugger показує що UI Thread чекає на Task
  • Task чекає на UI Thread

Діагностика у Visual Studio:

  1. Pause execution (Break All)
  2. Debug → Windows → Threads
  3. Подивіться call stack UI Thread — побачите .Result або .Wait()
  4. Подивіться call stack ThreadPool потоку — побачите continuation який чекає Dispatcher

Best Practices: Уникнення Deadlock

✅ DO:

  • Використовуйте async/await всюди (async all the way)
  • Додавайте ConfigureAwait(false) у бібліотечному коді
  • У ASP.NET Core можна безпечно використовувати .Result (але не рекомендовано)

❌ DON'T:

  • Ніколи .Result або .Wait() у UI коді
  • Ніколи .Result або .Wait() у ASP.NET Framework коді
  • Ніколи блокуючі виклики у async методах

5. ExecutionContext та AsyncLocal<T>

Окрім SynchronizationContext, є ще один важливий контекст який передається через awaitExecutionContext.

Що Таке ExecutionContext?

ExecutionContext — це контейнер для ambient data (контекстних даних) які автоматично передаються через async операції:

  • AsyncLocal<T> значення
  • Security context (identity, permissions)
  • Logical call context
// ExecutionContext автоматично передається через await
async Task Method1Async()
{
    // Встановлюємо значення
    MyAsyncLocal.Value = "Hello";
    
    await Method2Async();
}

async Task Method2Async()
{
    // Значення доступне тут!
    Console.WriteLine(MyAsyncLocal.Value);  // "Hello"
}

static AsyncLocal<string> MyAsyncLocal = new();

AsyncLocal<T> — Thread-Local для Async

AsyncLocal<T> — це аналог ThreadStatic але для async коду. Значення передається через await:

AsyncLocalExample.cs
static AsyncLocal<string> RequestId = new AsyncLocal<string>();

async Task ProcessRequestAsync(string requestId)
{
    // Встановлюємо RequestId для цього async flow
    RequestId.Value = requestId;
    
    Console.WriteLine($"[ProcessRequest] RequestId: {RequestId.Value}");
    
    await Step1Async();
    await Step2Async();
}

async Task Step1Async()
{
    await Task.Delay(100);
    
    // RequestId доступний тут!
    Console.WriteLine($"[Step1] RequestId: {RequestId.Value}");
}

async Task Step2Async()
{
    await Task.Delay(100);
    
    // І тут теж!
    Console.WriteLine($"[Step2] RequestId: {RequestId.Value}");
}

// Використання
await ProcessRequestAsync("REQ-001");
AsyncLocal Output
[ProcessRequest] RequestId: REQ-001
[Step1] RequestId: REQ-001
[Step2] RequestId: REQ-001

Use Case: Correlation ID для Логування

CorrelationId.cs
public class CorrelationContext
{
    private static AsyncLocal<string?> _correlationId = new AsyncLocal<string?>();
    
    public static string? CorrelationId
    {
        get => _correlationId.Value;
        set => _correlationId.Value = value;
    }
}

// ASP.NET Core Middleware
public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    
    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // Встановлюємо correlation ID для цього request
        CorrelationContext.CorrelationId = Guid.NewGuid().ToString();
        
        await _next(context);
    }
}

// Тепер у будь-якому місці коду можна отримати correlation ID
public class UserService
{
    private readonly ILogger<UserService> _logger;
    
    public async Task<User> GetUserAsync(int id)
    {
        // Correlation ID доступний автоматично!
        _logger.LogInformation(
            "Getting user {UserId}, CorrelationId: {CorrelationId}",
            id,
            CorrelationContext.CorrelationId
        );
        
        return await database.GetUserAsync(id);
    }
}

AsyncLocal vs ThreadStatic

Характеристика[ThreadStatic]AsyncLocal<T>
ScopeОдин потікAsync flow (через await)
Передається через await❌ Ні✅ Так
Передається через Task.Run❌ Ні✅ Так (за замовчуванням)
Use caseThread-specific dataRequest context, correlation ID

6. Практичні Завдання

Рівень 1: Deadlock Demo + Fix

Створіть WPF/WinForms додаток який демонструє deadlock та його виправлення.

Вимоги:

  • Кнопка "Deadlock" — викликає .Result і зависає
  • Кнопка "Correct" — використовує await і працює
  • Кнопка "ConfigureAwait Fix" — використовує ConfigureAwait(false) у бібліотечному методі

Рівень 2: AsyncLocal-based RequestContext

Створіть систему для передачі request context через async операції.

Вимоги:

  • RequestContext з AsyncLocal<T>
  • Автоматичне встановлення correlation ID
  • Логування з correlation ID у кожному методі
  • Демонстрація що context передається через await

Рівень 3: Custom SynchronizationContext

Створіть власний SynchronizationContext який логує всі Post/Send виклики.

Вимоги:

  • Наслідувати SynchronizationContext
  • Перевизначити Post() та Send()
  • Логувати кожен виклик з timestamp та thread ID
  • Встановити як SynchronizationContext.Current
  • Демонстрація роботи з async/await

Це завершує матеріал про SynchronizationContext та ConfigureAwait. Ви навчились розуміти контекст виконання, UI thread affinity, ConfigureAwait(false), deadlock scenarios та ExecutionContext з AsyncLocal.