Что мне делать с задачами async, которые я не хочу ждать?
Я пишу многопользовательский игровой сервер и смотрю, как новые функции С# async/await могут
Помоги мне. Ядром сервера является цикл, который обновляет всех участников в игре так же быстро, как и
могут:
while (!shutdown)
{
foreach (var actor in actors)
actor.Update();
// Send and receive pending network messages
// Various other system maintenance
}
Этот цикл необходим для обработки тысяч участников и обновления несколько раз в секунду, чтобы сохранить
игра работает гладко. Некоторые участники иногда выполняют медленные задачи в своих функциях обновления, таких как
как выборка данных из базы данных, где я хотел бы использовать async. После получения этих данных
актер хочет обновить состояние игры, которое должно быть выполнено в основном потоке.
Как это консольное приложение, я планирую написать SynchronizationContext, который может отправлять
ожидающих делегатов в основной цикл. Это позволяет этим задачам обновлять игру после завершения
и позволяет исключить необработанные исключения в основной цикл. Мой вопрос: как написать асинхронный
функции обновления? Это работает очень хорошо, но нарушает рекомендации не использовать async void:
Thing foo;
public override void Update()
{
foo.DoThings();
if (someCondition) {
UpdateAsync();
}
}
async void UpdateAsync()
{
// Get data, but let the server continue in the mean time
var newFoo = await GetFooFromDatabase();
// Now back on the main thread, update game state
this.foo = newFoo;
}
Я мог бы сделать update() async и протаить задачи обратно в основной цикл, но:
- Я не хочу добавлять накладные расходы на тысячи обновлений, которые никогда не будут использовать его.
- Даже в основном цикле я не хочу ждать задач и блокировать цикл.
- Ожидание задачи приведет к тупиковой ситуации, так как она должна завершиться в ожидающем потоке.
Что мне делать со всеми этими задачами, которые я не могу ждать? Единственный раз, когда мне захочется узнать, что они все
завершено, когда я закрываю сервер, но я не хочу собирать каждую задачу, созданную
возможно, недели обновлений.
Ответы
Ответ 1
Я понимаю, что суть этого в том, что вы хотите:
while (!shutdown)
{
//This should happen immediately and completions occur on the main thread.
foreach (var actor in actors)
actor.Update(); //includes i/o bound database operations
// The subsequent code should not be delayed
...
}
Если цикл while работает в основном потоке консоли. Это плотная однопоточная петля. Вы можете запускать foreach параллельно, но тогда вы все равно будете ждать самого длинного работающего экземпляра (операция привязки ввода-вывода для получения данных из базы данных).
Ожидать, что async не самый лучший вариант в этом цикле, вам нужно запустить эти задачи базы данных ввода-вывода в пуле потоков. В потоковом пуле async await было бы полезно освободить потоки пула.
Итак, следующий вопрос - как вернуть эти доработки в ваш основной поток. Ну, похоже, вам нужно что-то эквивалентное сообщению насоса на вашей основной нити. См. этот пост для получения информации о том, как это сделать, хотя это может быть немного тяжело. Вы можете просто иметь очередь завершения сортировки, которую вы проверяете на основном потоке в каждом проходе через ваш цикл Loop. Вы должны использовать одну из параллельных структур данных, чтобы сделать это, чтобы все было безопасно в потоке, а затем установить Foo, если он необходимо установить.
Кажется, что есть место для рационализации этого опроса актеров и потоков, но, не зная подробностей приложения, трудно сказать.
Несколько пунктов: -
-
Если у вас нет ожиданий выше задачи, ваш основной поток консоли выйдет, и ваше приложение тоже будет. Подробнее см. здесь.
-
Как вы уже указали, подождите, пока async не заблокирует текущий поток, но это означает, что код, следующий за ожиданием, будет выполняться только по завершении ожидания.
-
Заполнение может быть завершено или не может быть выполнено в вызывающем потоке. Вы уже упоминали контекст синхронизации, поэтому я не буду вдаваться в подробности.
-
Контекст синхронизации имеет значение null в приложении консоли. Подробнее см. здесь.
-
Async не работает для операций типа "огонь-и-забыть".
Для пожара и забывания вы можете использовать один из этих вариантов в зависимости от вашего сценария:
- Используйте Task.Run или Task.StartNew. См. здесь для различий.
- Используйте шаблон производителя/потребителя для сценариев с длинными сценариями, запущенных под вашим собственным файловым пулом.
Обратите внимание на следующее: -
- Чтобы вам пришлось обрабатывать исключения в ваших нерешенных задачах/потоках. Если есть какие-то исключения, которые вы не соблюдаете, вы можете обращаться с ними, даже для регистрации их происшествий. См. Информацию о ненаблюдаемых исключениях.
- Если ваш процесс умирает, когда эти длительные запущенные задачи находятся в очереди или запускаются, они не будут запущены, поэтому вам может понадобиться какой-то механизм сохранения (база данных, внешняя очередь, файл), которая отслеживает состояние этих операций,
Если вы хотите узнать о состоянии этих задач, вам нужно будет каким-то образом их отслеживать, будь то список в памяти или путем запроса очередей для вашего собственного пула потоков или путем запроса механизм сохранения. Самое приятное в механизме персистентности заключается в том, что он устойчив к сбоям и во время выключения вы можете сразу закрыть его, а затем забрать, когда вы закончили работу при перезагрузке (это, конечно, зависит от того, насколько важно, чтобы задачи выполнялись внутри определенный таймфрейм).
Ответ 2
Во-первых, я рекомендую вам не использовать свой собственный SynchronizationContext
; У меня есть одна доступная часть моей библиотеки AsyncEx, которую я обычно использую для консольных приложений.
Что касается методов обновления, они должны возвращать Task
. Моя библиотека AsyncEx имеет ряд "констант задачи", которые полезны, когда у вас есть метод, который может быть асинхронным:
public override Task Update() // Note: not "async"
{
foo.DoThings();
if (someCondition) {
return UpdateAsync();
}
else {
return TaskConstants.Completed;
}
}
async Task UpdateAsync()
{
// Get data, but let the server continue in the mean time
var newFoo = await GetFooFromDatabase();
// Now back on the main thread, update game state
this.foo = newFoo;
}
Возвращаясь к вашему основному циклу, решение там не совсем так понятно. Если вы хотите, чтобы каждый актер завершил игру до перехода к следующему актеру, вы можете сделать это:
AsyncContext.Run(async () =>
{
while (!shutdown)
{
foreach (var actor in actors)
await actor.Update();
...
}
});
В качестве альтернативы, если вы хотите запустить всех актеров одновременно и дождаться их завершения до перехода к следующему "тику", вы можете сделать это:
AsyncContext.Run(async () =>
{
while (!shutdown)
{
await Task.WhenAll(actors.Select(actor => actor.Update()));
...
}
});
Когда я говорю "одновременно" выше, он фактически запускает каждого актора по порядку, и поскольку все они выполняются в основном потоке (включая продолжения async
), фактического одновременного поведения нет; каждый "патрон кода" будет выполняться в одном и том же потоке.
Ответ 3
Я очень рекомендую посмотреть это видео или просто взглянуть на слайды:
Три основных совета по использованию Async в Microsoft Visual С# и Visual Basic
Из моего понимания, что вы, вероятно, должны делать в этом сценарии, возвращается Task<Thing>
в UpdateAsync
и, возможно, даже Update
.
Если вы выполняете некоторые асинхронные операции с 'foo' вне основного цикла, что происходит, когда асинхронная часть завершается во время будущего последовательного обновления? Я считаю, что вы действительно хотите дождаться завершения всех своих задач обновления и затем заменить свое внутреннее состояние за один раз.
В идеале вы сначала начнете все медленные (базы данных) обновления, а затем выполните другие более быстрые, чтобы весь набор был готов как можно скорее.