Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
При наличии синхронного метода в библиотеке, вы можете быть склонны предоставить асинхронный аналог, который оборачивает его в Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
В этой статье объясняется, почему этот подход почти всегда неправильный для библиотек и как думать о компромиссах.
Масштабируемость и разгрузка
Асинхронное программирование обеспечивает два различных преимущества:
- Масштабируемость — уменьшение потребления ресурсов путем освобождения потоков во время ожидания ввода-вывода.
- Разгрузка — перемещение работы в другой поток для поддержания отклика (например, свободного потока пользовательского интерфейса) или параллелизма.
Эти преимущества требуют различных подходов. Критическое различие: обертывание синхронного метода в Task.Run помогает с разгрузкой, но не делает ничего для масштабируемости.
Почему Task.Run не улучшает масштабируемость
Действительно асинхронная реализация уменьшает количество потоков, потребляемых во время длительной операции. Оболочка Task.Run по-прежнему блокирует поток— он просто перемещает блокировку из одного потока в другой:
public static class TimerExampleWrong
{
public static Task SleepAsync(int millisecondsTimeout)
{
return Task.Run(() => Thread.Sleep(millisecondsTimeout));
}
}
Public Module TimerExampleWrong
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
End Function
End Module
Сравните этот подход с действительно асинхронной реализацией, которая не потребляет потоки во время ожидания:
public static class TimerExampleRight
{
public static Task SleepAsync(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<bool>();
var timer = new Timer(
_ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);
tcs.Task.ContinueWith(
_ => timer.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
Public Module TimerExampleRight
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim tmr As New Timer(
Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)
tcs.Task.ContinueWith(
Sub(t) tmr.Dispose(), TaskScheduler.Default)
Return tcs.Task
End Function
End Module
Обе реализации выполняются после указанной задержки, но вторая реализация не блокирует поток во время ожидания. Для серверных приложений, обрабатывающих множество одновременных запросов, это различие напрямую влияет на количество запросов, которые сервер может обрабатывать одновременно.
Разгрузка является ответственностью потребителя
Обёртывание синхронных вызовов в Task.Run полезно для разгрузки потока пользовательского интерфейса. Однако потребитель, а не библиотека, должен обрабатывать эту обёртку.
public static class UIOffloadExample
{
public static int ComputeIntensive(int input)
{
int result = 0;
for (int i = 0; i < input; i++)
{
result += i;
}
return result;
}
public static async Task ConsumeFromUIThreadAsync()
{
int result = await Task.Run(() => ComputeIntensive(10_000));
Console.WriteLine($"Result: {result}");
}
}
Public Module UIOffloadExample
Public Function ComputeIntensive(input As Integer) As Integer
Dim result As Integer = 0
For i As Integer = 0 To input - 1
result += i
Next
Return result
End Function
Public Async Function ConsumeFromUIThreadAsync() As Task
Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
Console.WriteLine($"Result: {result}")
End Function
End Module
Потребитель знает свой контекст: находятся ли они в потоке пользовательского интерфейса, какая степень детализации им нужна, и будет ли полезна разгрузка. Библиотека не работает.
Почему библиотеки не должны предоставлять асинхронные оболочки синхронизации
Если библиотека предоставляет только синхронный метод (а не асинхронную оболочку), потребители выигрывают несколькими способами:
- Сокращенная область поверхности API: меньше методов для изучения, тестирования и обслуживания.
- Нет вводящих в заблуждение ожиданий масштабируемости: пользователи знают, что только методы, предоставляемые как асинхронные, фактически обеспечивают преимущества масштабируемости.
-
Контроль потребителей: вызывающие абоненты выбирают, следует ли и как выгружать данные, с нужным уровнем детализации. Приложение сервера с высокой пропускной способностью может вызывать синхронный метод напрямую, избегая ненужных издержек.
Task.Run - Улучшенная производительность: асинхронные оболочки добавляют затраты на выделение, переключение контекста и планирование пула потоков. Для высокодетализированных операций накладные расходы могут быть значительными.
Исключения в правиле
Некоторые базовые классы предоставляют асинхронные методы, чтобы производные классы могли переопределить их с помощью по-настоящему асинхронных реализаций. Базовый класс предоставляет асинхронную синхронизацию по умолчанию.
Например, Stream предоставляет ReadAsync и WriteAsync. Базовые реализации оборачивают синхронные Read и Write методы. Производные классы, такие как FileStream и NetworkStream переопределяют эти методы с помощью асинхронных реализаций ввода-вывода, которые обеспечивают реальные преимущества масштабируемости.
Аналогичным образом, TextReader предоставляет ReadToEndAsync на базовом классе в качестве оболочки, а StreamReader переопределяет её с действительно асинхронной реализацией, которая внутренне вызывает ReadAsync.
Эти исключения допустимы, так как:
- Шаблон предназначен для полиморфизма. Вызывающие взаимодействуют с базовым типом.
- Производные типы действительно предоставляют асинхронные переопределения.
Рекомендация
Предоставление асинхронных методов из библиотеки только в том случае, если реализация обеспечивает реальные преимущества масштабируемости по сравнению с синхронным аналогом. Не предоставляйте асинхронные методы исключительно для разгрузки. Оставьте этот выбор потребителю.