작업할 때 async과(와) await은(는) 두 가지 컨텍스트 형식으로서 중요한 역할을 하며, 그 역할은 매우 다릅니다. ExecutionContext와 SynchronizationContext. 각 요소가 무엇을 하는지, 각 요소가 async/await와 어떻게 상호작용하는지, 그리고 왜 SynchronizationContext.Current이 대기 포인트를 넘어 흐르지 않는지를 배우게 됩니다.
ExecutionContext란?
ExecutionContext 는 프로그램의 논리적 제어 흐름과 함께 흐르는 앰비언트 상태의 컨테이너입니다. 동기 환경에서 앰비언트 정보는 TLS(스레드 로컬 스토리지)에 있으며 지정된 스레드에서 실행되는 모든 코드는 해당 데이터를 봅니다. 비동기 환경에서 논리 작업은 한 스레드에서 시작하여 다른 스레드에서 일시 중단 및 다시 시작할 수 있습니다. 스레드-로컬 데이터는 자동으로ExecutionContext 따르지 않으므로 따라가게 됩니다.
ExecutionContext 흐름 방법
ExecutionContext.Capture()을 사용하여 ExecutionContext를 캡처합니다. 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
.NET의 모든 비동기 API(Run, QueueUserWorkItem, BeginRead 등)은 ExecutionContext을(를) 캡처하고 콜백을 호출할 때 저장된 컨텍스트를 사용합니다. 한 스레드에서 상태를 캡처하고 다른 스레드에서 복원하는 이 프로세스는 "Flowing ExecutionContext"의 의미입니다.
SynchronizationContext란?
SynchronizationContext 는 작업을 실행하려는 대상 환경을 나타내는 추상화입니다. 다른 UI 프레임워크는 자체 구현을 제공합니다.
- Windows Forms는
Control.BeginInvoke을(를) 제공하며, 이는 Post을(를) 재정의하여WindowsFormsSynchronizationContext을(를) 호출하도록 합니다. - WPF는
DispatcherSynchronizationContext을 재정의하여 Post를 호출하는Dispatcher.BeginInvoke를 제공합니다. - ASP.NET(.NET Framework)에서는
HttpContext.Current사용할 수 있는 고유한 컨텍스트를 제공했습니다.
프레임워크별 마샬링 API 대신 사용하여 SynchronizationContext 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
SynchronizationContext를 캡처하기
SynchronizationContext를 캡처할 때, SynchronizationContext.Current의 참조를 읽고 나중에 사용하기 위해 저장합니다. 그런 다음 Post을 호출하여 캡처된 참조에서 해당 환경으로 작업을 다시 예약합니다.
Flowing ExecutionContext와 SynchronizationContext 사용 비교
두 메커니즘 모두 스레드에서 상태를 캡처하는 것을 포함하지만 다른 용도로 사용됩니다.
- Flowing ExecutionContext 는 대리자를 실행하는 동안 앰비언트 상태를 캡처하고 동일한 상태를 현재 상태로 만드는 것을 의미합니다. 대리자는 어디에서 실행되든지 실행됩니다. 상태가 이를 따릅니다.
- SynchronizationContext를 사용하는 것은 예약 대상을 캡처하고 이를 사용하여 대리자가 실행되는 위치를 결정하는 것을 의미합니다. 대리자가 실행되는 위치를 지정하는 캡처된 컨텍스트입니다.
요컨대, ExecutionContext "어떤 환경을 표시해야 하나요?"라고 대답하고 SynchronizationContext "코드는 어디에서 실행해야 하나요?" 라고 대답합니다.
비동기/await가 두 컨텍스트와 상호 작용하는 방법
이 인프라는 async/await 자동으로 두 컨텍스트와 상호 작용하지만, 각각 다른 방식으로 의사소통합니다.
ExecutionContext는 항상 전달됩니다.
메서드가 대기자의 IsCompleted가 false를 반환하기 때문에 일시 중단될 때마다, 인프라는 ExecutionContext를 캡처합니다. 메서드가 다시 시작되면 캡처된 컨텍스트 내에서 연속이 실행됩니다. 이 동작은 비동기 메서드 작성기(예 AsyncTaskMethodBuilder: )에 기본 제공되며 어떤 종류의 대기 가능 파일을 사용하든 관계없이 적용됩니다.
SuppressFlow()가 존재하지만, ConfigureAwait(false)처럼 대기 기능에 특화되어 있지는 않습니다. 억제가 활성 상태인 동안 큐에 대기하는 작업에 대한 ExecutionContext 캡처를 억제합니다. 비동기 메서드 작성기에서 연속 작업을 위해 캡처된 await 복원을 건너뛰도록 지시하는 프로그래밍 모델별ExecutionContext 옵션은 제공하지 않습니다. 이러한 디자인은 비동기 환경에서 스레드-로컬 의미 체계를 시뮬레이션하는 인프라 수준 지원이므로 의도적이며 ExecutionContext 대부분의 개발자는 이를 생각할 필요가 없습니다.
작업 대기자가 동기화 컨텍스트를 캡처합니다.
SynchronizationContext는 Task 및 Task<TResult> 지원을 포함합니다. 비동기 메서드 작성기에서는 이 지원을 포함하지 않습니다.
작업을 await하는 경우:
- "대기자가 SynchronizationContext.Current을(를) 확인합니다."
- 컨텍스트가 있으면 awaiter가 이를 캡처합니다.
- 작업이 완료되면 완료된 스레드 또는 스레드 풀에서 실행되는 대신 캡처된 컨텍스트에 연속 작업이 다시 게시됩니다.
이 동작은 "당신이 있던 곳으로 다시 데려온다"는 것입니다 await . 예를 들어 데스크톱 애플리케이션의 UI 스레드에서 다시 시작합니다.
ConfigureAwait는 SynchronizationContext 캡처를 제어합니다.
마샬링 동작을 원하지 않으면 ConfigureAwait를 false와 함께 호출하세요.
await task.ConfigureAwait(false);
당신이 continueOnCapturedContext을(를) false으로 설정할 때, awaiter는 SynchronizationContext를 검사하지 않고 연속 작업은 태스크가 완료된 위치(일반적으로 스레드 풀 스레드에서)에서 실행됩니다. 라이브러리 작성자는 코드가 캡처된 컨텍스트에서 다시 시작해야 하는 특별한 필요가 없는 한, 모든 await에서 ConfigureAwait(false)를 사용해야 합니다.
SynchronizationContext.Current가 await 간에 전파되지 않음
이 점은 가장 중요합니다. SynchronizationContext.Current 대기 지점을 가로질러 흐르지 않습니다. 런타임의 비동기 메서드 작성기는 내부 오버로드를 사용하여, 흐름의 일부인 ExecutionContext에 SynchronizationContext가 전달되지 않도록 명시적으로 방지합니다.
이 문제가 중요한 이유
기술적으로, SynchronizationContext는 ExecutionContext이 포함할 수 있는 하위 컨텍스트 중 하나입니다. 어떤 코드가 ExecutionContext의 일부로 흐르면, 스레드 풀 스레드에서 실행되는 코드는 컨텍스트가 흐름을 통해 "전파"되었기 때문에 UI를 Current로 볼 수 있으며, 이는 해당 스레드가 UI SynchronizationContext 스레드이기 때문이 아닙니다. 이러한 변화는 "현재 환경"에서 "역사적으로 콜 체인 어딘가에 존재했던 환경"으로의 의미를 SynchronizationContext.Current 바꿀 것입니다.
Task.Run 예제
스레드 풀로 작업을 위임하는 코드를 고려해보세요. 여기에 설명된 UI 스레드 동작은 UI 앱과 같이 null이 아닌 경우에만 SynchronizationContext.Current 적용됩니다.
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
콘솔 앱에서는 SynchronizationContext.Current이(가) 일반적으로 null이므로, 코드 조각이 실제 UI 스레드에서 다시 시작되지 않습니다. 대신, 이 코드 조각은 규칙을 개념적으로 보여 줍니다. UI SynchronizationContext가 await 포인트를 따라 흐르면, Task.Run에 전달된 대리자 내부의 await는 해당 UI 컨텍스트를 Current로 간주합니다. 이후의 await DownloadAsync() 연속은 UI 스레드로 다시 전달되어 Compute(data)이 스레드 풀 대신에 UI 스레드에서 실행됩니다. 이 동작은 호출의 목적을 무찌릅니다 Task.Run .
런타임에서 SynchronizationContext의 흐름을 ExecutionContext에서 억제하기 때문에, Task.Run 내부의 await는 외부 UI 컨텍스트를 상속하지 않으며, 연속 작업은 의도한 대로 스레드 풀에서 계속 실행됩니다.
요약
| Aspect | 실행 컨텍스트 | SynchronizationContext |
|---|---|---|
| Purpose | 비동기 경계를 넘어 주변 상태를 전달합니다. | 대상 스케줄러(코드가 실행되어야 하는 위치)를 나타냅니다. |
| 에 의해 캡처됨 | 비동기 메서드 작성기(인프라) | 작업 대기자(await task) |
| 대기를 통한 흐름? | 예, 항상 | 아니요- 캡처 및 게시됨, 흐름되지 않음 |
| 억제 API |
ExecutionContext.SuppressFlow (고급, 거의 필요하지 않은 경우) |
ConfigureAwait(false) |
| Scope | 모든 대기 대상 |
Task 및 Task<TResult> (사용자 지정 대기자는 유사한 논리를 추가할 수 있습니다.) |
참고하십시오
.NET