从Windows服务调用异步方法

时间:2013-12-14 16:22:53

标签: c# asynchronous windows-services async-await

我有一个用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需要相对快速地返回。 (作业循环需要在一个单独的线程上运行。)想到几个想法:

  • 通过将该处理程序放在Main?
  • ,我是否可以很好地涵盖未观察到的异常
  • 使用Task.Run会有什么好处,比如Task.Run(() => _scheduler.StartLoopAsync().Wait());
  • 在此处拨打_scheduler.StartLoopAsync().ConfigureAwait(false)会有什么好处吗? (我怀疑它,因为这里没有await。)
  • 在这种情况下使用Stephen Cleary's AsyncContextThread会有什么好处吗?我没有看到任何使用它的例子,因为我开始一个无限循环,我不知道同步回到某些上下文甚至是相关的。

2 个答案:

答案 0 :(得分:11)

对于所有未观察到的UnobservedTaskException例外,将调用

Task,因此这是一个像这样记录的好地方。但是,它不是很好,因为根据您的程序逻辑,您可能会看到虚假消息;例如,如果您Task.WhenAny然后忽略较慢的任务,则应忽略来自该较慢任务的任何异常,但它们会被发送到UnobservedTaskException。作为替代方案,请考虑在您的顶级任务(从ContinueWith返回的任务)上放置StartLoopAsync

您对StartLoopAsync的调用对我来说很好,假设它是正确异步的。你可以使用TaskRun(例如,Task.Run(() => _scheduler.StartLoopAsync()) - 不需要Wait),但唯一的好处是如果StartLoopAsync本身可以引发异常(而不是错误的返回任务)或者如果在第一个await之前花了太长时间。

ConfigureAwait(false)仅在您推测await时才有用。

我的AsyncContextThread是针对这种情况而设计的,但它的设计也非常简单。 :) AsyncContextThread提供了一个独立的线程,其主循环类似于您的调度程序,包含TaskSchedulerTaskFactorySynchronizationContext。但是,它很简单:它只使用一个线程,并且所有调度/上下文都指向同一个线程。我喜欢这样,因为它极大地简化了线程安全问题,同时也允许并发异步操作 - 但它没有充分利用线程池,因此,例如,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);
        }
    }
}