【问题标题】:Calling async methods from a Windows Service从 Windows 服务调用异步方法
【发布时间】:2013-12-14 16:22:52
【问题描述】:

我有一个用 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 会有什么好处吗?我还没有看到任何使用它的例子,而且由于我正在启动一个无限循环,我不知道同步备份到某些上下文在这里甚至是相关的。

【问题讨论】:

  • 下周我会写一个AsyncContextThread 的例子。
  • 所以我不再纠缠你了? ;) 不用担心,我很期待,再次感谢您的帮助!
  • 没问题。实际上,我只是从很久以前就看到了您对 CodePlex 的评论,出于某种原因,我没有收到通知...
  • 嘿@StephenCleary,您有没有机会向我们介绍如何在Windows 服务中使用AsyncContextThread 的示例?我不清楚是应该从 OnStart() 还是从 Main() 调用它。
  • @N1njaB0b: 是的,我从来没有抽空写下来……它还在我的待办事项清单上……你应该从OnStart 调用它,然后在@ 中加入它987654331@.

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


【解决方案1】:

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 可以让您删除/简化您已经编写的一些代码。但另一方面,它不像您的解决方案那样是多线程的。

【讨论】:

    【解决方案2】:

    本身不是一个答案,但在发布此问题一年后,我们将此服务移至 Azure 云服务。我发现Azure SDK 的 Worker Role 模板是从同步中正确调用异步、提供取消支持、处理异常等的一个很好的示例。它与 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);
            }
        }
    }
    

    【讨论】:

    • 我认为如果将 cancelToken 传递给 Task.Delay 方法会更好,这样如果请求取消,即使 RunAsync 方法当前正在等待它也可以立即发生。
    猜你喜欢
    • 2014-04-30
    • 1970-01-01
    • 1970-01-01
    • 2014-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多