Асинхронное и многопоточное программирование¶
В этой статье собраны рекомендации по написанию асинхронного и многопоточного кода.
Т.к. наша система является нагруженной и многопользовательской, код мы должны писать таким образом,
чтобы приложения справлялись с нагрузкой.
Поэтому активно используем асинхронное программирование с помощью любимых async/await
.
Важно помнить, что сам по себе
async/await
не делает код более быстрым: смена контекста,state machine
, все дела.
В веб приложениях польза async/await
огромна, т.к. позволяет увеличить производительность приложения за счет переиспользования потоков из пула
во время выполнения I/O
операций. Поэтому в вебе - все async
. Либы, которые используются в вебе - тоже async
.
Рекомендации¶
Весь стек вызова должен быть асинхронным¶
Если метод вызывает другой асинхронный метод, то он тоже должен быть асинхронным и не должен блокировать поток, пока ожидается результат выполнения
// неправильно
public int GetCount()
{
return GetCountInternalAsync().GetAwaiter().GetResult();
}
Асинхронный метод не должен возвращать void¶
В случае ошибки в асинхронном методе может упасть весь процесс, т.к. ошибка не обрабатывается.
Если очень нужно написать такой метод, то всегда используйте ContinueWith
:
public void LongRunningOperation()
{
var task = InternalLongRunningOperaion();
task.ContinueWith(handler, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted);
}
Использовать CancellationToken
¶
Должна быть возможность остановить асинхронную операцию (например, наш асинхронный метод, который внутри делает кучу всего). Это нужно при остановке приложения, таймауте.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(_sleepTime, stoppingToken);
}
}
Использовать Task.FromResult
, если нужно синхронный метод сделать асинхронным¶
Иногда интерфейс требует, чтобы методы был асинхронным, но по факту операция может выполниться синхронно.
Для этого надо использовать Task.FromResult
, а не запускать новую таску через Task.Run
,
т.к. не будет создаваться новый поток (или браться из пула):
// неправильно
public Task<int> GetCountAsync()
{
return Task.Run(() => _count);
}
// правильно
public Task<int> GetCountAsync()
{
return Task.FromResult(_count);
}
Если нужно вернуть просто
Task
, то пользуйтесьTask.FromResult<bool>(true)
, т.к. нет методаTask.FromResult()
.
Тюнинг и мониторинг пула потоков¶
Наши приложения нагружены, поэтому нам нужно сразу настроить пул потоков и следить за их состоянием.
Все для этого есть в nuget
пакете Curiosity.Utils.Hosting
.
Требуется актуализация пакета.
Использовать Task.Yeild
, если асинхронный метод может долго выполняться синхронно перед тем, как будет вызван асинхронный метод¶
Например, наш асинхронный метод разгребает очередь, построенную на базе BlockingCollection
.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// если этого нет, то поток будет заблокирован, пока _notificationQueue.Take не вернет результат
await Task.Yield();
while (!stoppingToken.IsCancellationRequested)
{
var queueItem = _notificationQueue.Take(stoppingToken);
var notifications = queueItem.Notifications;
await ProcessNotificationAsync(notifications);
}
}
При первом вызове метода ExecuteAsync
вызывающий поток будет заблокирован, пока в очереди не появятся первый элемент. Это плохо.
Task.Yield
принудительно вызывает смену контекста, что приводит к освобождению вызывающего потока.
Не запускать долгие вычислительные операции в отдельном потоке¶
Асинхронность хороша для выполнения I/O bound
операций, поэтому использовать поток из пула для вычислительной операции не рекомендуется.
Если очень хочется, то можно запустить с опцией TaskCreationOptions.LongRunning
:
public void StartOperation()
{
Task.Factory.StartNew(LongRunningOperation, TaskCreationOptions.LongRunning);
}
public void LongRunningOperation()
{
}
Всегда вызывать FlushAsync
, если асинхронно работаете с любым наследником Stream
¶
Даже если используете поток в using
, всегда это нужно делать, иначе забуферизированные данные не попадут туда, куда мы хотели.
Если метод внутри только вызывает другой асинхронный метод, не нужно делать на нем await
¶
Мы экономим на пареключении потоков, не создается машина состояния, меньше нагрузка на GC
.
Исключение: если вызов обернут в
try-finally
, иначеfinally
отработает некорректно. Еще одно исключение: если внутри используетсяusing
.
public Task<int> GetCountAsync()
{
return GetCountInternalAsync();
}
Замеры производительности есть в этой статье.
Не работайте с непотокобезопасным кодом в нескольких потоках¶
Тут все очевидно.