我有一个用C#编写的Windows服务,会定期触发后台作业。通常,在任何给定时间,几十个重度I / O绑定任务(下载大文件等)并行运行。该服务在相对繁忙的Web服务器上运行(目前是必需的),我认为在线程保护方面可以尽可能地使用异步API。
大部分工作已经完成。所有作业现在完全异步(利用HttpClient等),主要作业循环(使用大量Task.Delay)也是如此。剩下的就是弄清楚如何从服务的OnStart正确安全地启动主循环。实际上,这是一个备受关注的呼叫异步同步困境。以下是我到目前为止(非常简化)。
在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需要相对快速地返回。 (作业循环需要在一个单独的线程上运行。)想到几个想法:
Task.Run(() => _scheduler.StartLoopAsync().Wait());
?_scheduler.StartLoopAsync().ConfigureAwait(false)
会有什么好处吗? (我怀疑它,因为这里没有await
。)答案 0 :(得分:11)
UnobservedTaskException
例外,将调用 Task
,因此这是一个像这样记录的好地方。但是,它不是很好,因为根据您的程序逻辑,您可能会看到虚假消息;例如,如果您Task.WhenAny
然后忽略较慢的任务,则应忽略来自该较慢任务的任何异常,但它们会被发送到UnobservedTaskException
。作为替代方案,请考虑在您的顶级任务(从ContinueWith
返回的任务)上放置StartLoopAsync
。
您对StartLoopAsync
的调用对我来说很好,假设它是正确异步的。你可以使用TaskRun
(例如,Task.Run(() => _scheduler.StartLoopAsync())
- 不需要Wait
),但唯一的好处是如果StartLoopAsync
本身可以引发异常(而不是错误的返回任务)或者如果在第一个await
之前花了太长时间。
ConfigureAwait(false)
仅在您推测await
时才有用。
我的AsyncContextThread
是针对这种情况而设计的,但它的设计也非常简单。 :) AsyncContextThread
提供了一个独立的线程,其主循环类似于您的调度程序,包含TaskScheduler
,TaskFactory
和SynchronizationContext
。但是,它很简单:它只使用一个线程,并且所有调度/上下文都指向同一个线程。我喜欢这样,因为它极大地简化了线程安全问题,同时也允许并发异步操作 - 但它没有充分利用线程池,因此,例如,CPU绑定工作会阻塞主循环(类似于UI线程场景)。
在您的情况下,听起来AsyncContextThread
可能会让您删除/简化您已编写的部分代码。但另一方面,它不像你的解决方案那样是多线程的。
答案 1 :(得分:6)
本身不是答案,但在发布此问题一年后,我们将此服务移至Azure云服务。我发现Azure SDK的工作者角色模板是从同步中正确调用异步,提供取消支持,处理异常等的一个很好的示例。它并不完全与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);
}
}
}