Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
При работе с async и await, типы контекста ExecutionContext и SynchronizationContext играют важные, но очень разные роли. Вы узнаете, что делает каждый из них, как каждый из них взаимодействует async/await, и почему SynchronizationContext.Current не проходит через точки ожидания.
Что такое ExecutionContext?
ExecutionContext — это контейнер для сопутствующего состояния, который распространяется с логическим потоком управления вашей программы. В синхронном мире внешние данные находятся в локальном хранилище потоков (TLS), а весь код, выполняемый в данном потоке, видит эти данные. В асинхронном мире логическая операция может начаться в одном потоке, затем приостановиться и возобновиться в другом потоке. Данные потока не следуют автоматически — ExecutionContext заставляет их следовать.
Как выполняется передачa ExecutionContext
Захват ExecutionContext с помощью ExecutionContext.Capture(). Восстановите его во время выполнения делегата с помощью 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
Все асинхронные API в .NET, которые разделяют работу — Run, QueueUserWorkItem, BeginRead и другие — захватывают ExecutionContext и используют хранимый контекст для обратного вызова. Этот процесс записи состояния в одном потоке и его восстановление на другом — это то, что означает "поток ExecutionContext".
Что такое SynchronizationContext?
SynchronizationContext — это абстракция, представляющая целевую среду, в которой требуется выполнить работу. Различные платформы пользовательского интерфейса предоставляют собственные реализации:
- Windows Forms предоставляет
WindowsFormsSynchronizationContext, который переопределяет Post, чтобы вызватьControl.BeginInvoke. - WPF предоставляет
DispatcherSynchronizationContext, который переопределяет Post для вызоваDispatcher.BeginInvoke. - ASP.NET (в .NET Framework) предоставил собственный контекст, обеспечивающий доступность
HttpContext.Current.
Используя SynchronizationContext вместо маршалинга API, специфичных для фреймворков, можно создавать компоненты, работающие в различных фреймворках пользовательского интерфейса.
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.Current и сохраняете ее для последующего использования. Затем вы вызовете Post по захваченной ссылке, чтобы запланировать работу обратно в эту среду.
Передача ExecutionContext в сравнении с использованием SynchronizationContext
Хотя оба механизма включают фиксацию состояния из потока, они предназначены для разных целей.
- Поток ExecutionContext означает запись внешнего состояния и создание того же состояния во время выполнения делегата. Делегат запускается в том месте, где он окажется, — состояние следует за ним.
- Использование SynchronizationContext означает захват цели планирования и его использование для определения места выполнения делегата. Захваченный контекст определяет, где выполняется делегат.
Короче говоря: ExecutionContext отвечает на вопрос "какая среда должна быть видна?", а SynchronizationContext отвечает на вопрос "где должен выполняться код?"
Взаимодействие конструкций async и await с обоими контекстами
Инфраструктура async/await взаимодействует с обоими контекстами автоматически, но по-разному.
ExecutionContext всегда распространяется
При приостановке метода await (так как средство ожидания IsCompleted возвращает false), инфраструктура захватывает состояние ExecutionContext. При возобновлении метода продолжение выполняется в захваченном контексте. Это поведение встроено в построители асинхронных методов (например, AsyncTaskMethodBuilder) и применяется независимо от того, какой тип ожидаемого элемента вы используете.
SuppressFlow() существует, но это не специфичный для await переключатель, как ConfigureAwait(false). Он подавляет захват данных для работы, которую вы ставите в очередь, пока подавление активно. Он не предоставляетawait параметр модели программирования, который сообщает асинхронным конструкторам методов пропустить восстановление захваченного ExecutionContext для продолжения. Этот дизайн намеренно выбран, потому что ExecutionContext - это поддержка инфраструктурного уровня, которая симулирует семантику локальности потока в асинхронном мире, и большинству разработчиков никогда не нужно об этом задумываться.
Функция ожидания задач захватывает СинхронизациюContext
Ожидатели для Task и Task<TResult> включают поддержку SynchronizationContext. Построители асинхронных методов не включают эту поддержку.
await При выполнении задачи:
- Проверка ожидателя SynchronizationContext.Current.
- Если контекст существует, ожидающий улавливает его.
- По завершении задачи продолжение будет размещено обратно в этот захваченный контекст, а не на завершенном потоке или пуле потоков.
Это поведение, как await "возвращает вас обратно к тому, где вы были". Например, возобновление работы на потоке пользовательского интерфейса в настольном приложении.
Конструкция ConfigureAwait управляет захватом SynchronizationContext
Если вы не хотите поведение маршалинга, вызовите ConfigureAwait с использованием false.
await task.ConfigureAwait(false);
Если задано значение continueOnCapturedContext, false, средство ожидания не проверяет наличие SynchronizationContext, и продолжение выполняется там, где бы ни выполнялась задача (обычно в потоке пула потоков). Авторы библиотек должны использовать ConfigureAwait(false) при каждом использовании await, если только код не должен возобновиться в захваченном контексте.
SynchronizationContext.Current не переносится через await-выражения
Эта точка является наиболее важной: SynchronizationContext.Currentне проходит между точками ожидания. Построители асинхронных методов в среде выполнения используют внутренние перегрузки, которые явно подавляют распространение SynchronizationContext как часть ExecutionContext.
Почему это важно
Технически, SynchronizationContext — один из вложенных контекстов, которые ExecutionContext может содержать. Если контекст перетекал как часть ExecutionContext, код, выполняемый в потоке пула потоков, может воспринимать пользовательский интерфейс SynchronizationContext как Current, не потому что этот поток является потоком пользовательского интерфейса, а потому что контекст "утёк" через поток. Это изменение изменит значение SynchronizationContext.Current из "среды, в которой я в настоящее время находится" на "окружающую среду, которая исторически существовала где-то в цепочке вызовов".
Пример Task.Run
Рассмотрим код, который передает работу в пул потоков. Поведение потока пользовательского интерфейса, описанное здесь, применяется только, если 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, поэтому фрагмент кода не возобновляется в настоящем потоке пользовательского интерфейса. Вместо этого фрагмент кода концептуально иллюстрирует правило: если пользовательский интерфейс SynchronizationContext передается через await точки, то await внутри передаваемого делегата будет воспринимать этот контекст пользовательского интерфейса как Current. Продолжение после await DownloadAsync() затем будет возвращаться в поток пользовательского интерфейса, чтобы Compute(data) выполнялся в потоке пользовательского интерфейса, а не в пуле потоков. Это поведение сводит на нет цель Task.Run вызова.
Поскольку среда выполнения подавляет SynchronizationContext поток в ExecutionContext, await внутри Task.Run не наследует внешний контекст пользовательского интерфейса, и выполнение продолжения продолжается в пуле потоков, как и должно.
Сводка
| Аспект | Контекст выполнения | SynchronizationContext |
|---|---|---|
| Purpose | Переносит контекстное состояние через асинхронные границы | Представляет целевой планировщик (где должен выполняться код) |
| Захвачено | Конструкторы асинхронных методов (инфраструктура) | Ожидатели задач (await task) |
| Потоки в ожидании? | Да, всегда | Нет—записано и размещено, а не передано потоком |
| API супрессии |
ExecutionContext.SuppressFlow (расширенный; редко требуется) |
ConfigureAwait(false) |
| Объем | Все объекты, которые можно ожидать |
Task и Task<TResult> (пользовательские ожидатели могут добавлять аналогичную логику) |