Вызов методов async из службы Windows
У меня есть служба Windows, написанная на С#, которая периодически запускает фоновые задания. Как правило, в любой момент времени параллельно работают несколько десятков сильно связанных задач ввода-вывода (загрузка больших файлов и т.д.). Служба работает на относительно занятом веб-сервере (необходимо на данный момент), и я думаю, что это может значительно помочь с точки зрения сохранения потоков для использования асинхронных API как можно больше.
Большая часть этой работы выполнена. Все задания теперь полностью асинхронны (использование HttpClient и т.д.), А также основной цикл работы (с большими дозами Task.Delay). Осталось выяснить, как правильно и безопасно запускать основной цикл из сервиса OnStart. Essentialy, это предостережение о дилемме "вызов-асинхронная синхронизация". Ниже я до сих пор (сильно упрощен).
в Program.cs:
static void Main(string[] args) {
TaskScheduler.UnobservedTaskException += (sender, e) => {
// log & alert!
e.SetObserved();
};
ServiceBase.Run(new MyService());
}
в MyService.cs:
protected override void OnStart(string[] args) {
_scheduler.StartLoopAsync(); // fire and forget! will this get me into trouble?
}
Это вызов, который вызывает StartLoopAsync
, который касается меня. Я не могу просто Wait()
вернуть возвращаемую задачу, потому что OnStart должен вернуться относительно быстро. (Петли задания должны запускаться по отдельному потоку.) Приходят в голову пару соображений:
- Я хорошо освещен до незаметных исключений, разместив этот обработчик в Main?
- Будет ли какая-либо польза от использования Task.Run, что-то вроде
Task.Run(() => _scheduler.StartLoopAsync().Wait());
?
- Будет ли какая-нибудь польза для вызова
_scheduler.StartLoopAsync().ConfigureAwait(false)
здесь? (Я сомневаюсь, так как здесь нет await
.)
- Будет ли какая-либо польза от использования Stephen Cleary AsyncContextThread в этой ситуации? Я не видел никаких примеров использования этого, и поскольку я начинаю бесконечный цикл, я не знаю, что синхронизация обратно в какой-то контекст даже имеет значение здесь.
Ответы
Ответ 1
UnobservedTaskException
будет вызван для всех незаметных исключений Task
, поэтому это хорошее место для ведения журнала, как это. Однако это не очень удобно, потому что в зависимости от вашей логики программы вы можете видеть ложные сообщения; например, если вы Task.WhenAny
и затем игнорируете более медленную задачу, тогда любые исключения из этой более медленной задачи должны игнорироваться, но они отправляются на UnobservedTaskException
. В качестве альтернативы рассмотрим размещение ContinueWith
в задаче верхнего уровня (тот, который возвращается из StartLoopAsync
).
Ваш вызов StartLoopAsync
выглядит хорошо для меня, считая его асинхронным. Вы можете использовать TaskRun
(например, Task.Run(() => _scheduler.StartLoopAsync())
- no Wait
), но единственным преимуществом было бы, если бы StartLoopAsync
сам мог вызвать исключение (в отличие от отказа от возвращаемой задачи), или если он тоже задолго до первого await
.
ConfigureAwait(false)
полезен только при выполнении await
, как вы предполагали.
My AsyncContextThread
предназначен для такого рода ситуаций, но он также был разработан очень просто.:) AsyncContextThread
предоставляет независимый поток с основным контуром, похожим на ваш планировщик, в комплекте с TaskScheduler
, TaskFactory
и SynchronizationContext
. Однако это просто: он использует только один поток, и все точки планирования/контекста возвращаются к тому же потоку. Мне это нравится, потому что это значительно упрощает проблемы безопасности потоков, а также позволяет одновременные асинхронные операции, но не полностью использует пул потоков, поэтому, например, работа с привязкой к ЦП блокирует основной цикл (аналогичный сценарию потока пользовательского интерфейса).
В вашей ситуации кажется, что AsyncContextThread
может позволить вам удалить/упростить часть кода, который вы уже написали. Но, с другой стороны, он не многопоточен, как ваше решение.
Ответ 2
Не ответ сам по себе, но через год после публикации этого вопроса мы переносим эту услугу на службу Azure Cloud. Я нашел шаблон Azure SDK, чтобы быть хорошим примером правильного вызова async из синхронизации, предоставления поддержки отмены, обработки исключений, и т.д. Это не совсем яблоки с яблоками с услугами Windows, поскольку последний не обеспечивает эквивалент метода Run
(вам нужно начать свою работу в OnStart
и сразу же вернуться), но для чего это стоит, вот он:
public class WorkerRole : RoleEntryPoint
{
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false);
public override void Run() {
Trace.TraceInformation("WorkerRole1 is running");
try {
this.RunAsync(this.cancellationTokenSource.Token).Wait();
}
finally {
this.runCompleteEvent.Set();
}
}
public override bool OnStart() {
// Set the maximum number of concurrent connections
ServicePointManager.DefaultConnectionLimit = 12;
// For information on handling configuration changes
// see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.
bool result = base.OnStart();
Trace.TraceInformation("WorkerRole1 has been started");
return result;
}
public override void OnStop() {
Trace.TraceInformation("WorkerRole1 is stopping");
this.cancellationTokenSource.Cancel();
this.runCompleteEvent.WaitOne();
base.OnStop();
Trace.TraceInformation("WorkerRole1 has stopped");
}
private async Task RunAsync(CancellationToken cancellationToken) {
// TODO: Replace the following with your own logic.
while (!cancellationToken.IsCancellationRequested) {
Trace.TraceInformation("Working");
await Task.Delay(1000);
}
}
}