如何在基于 C# 的 Windows 服务中以不同的时间间隔并行处理多个任务 运行?

How to handle multiple tasks running in parallel at different intervals inside a C# based Windows service?

我已经有一些在 Windows 中使用线程的经验,但大部分经验来自在 C/C++ 应用程序中使用 Win32 API 函数。然而,当谈到 .NET 应用程序时,我常常不确定如何正确处理多线程。有线程、任务、TPL 和其他各种我可以用于多线程的东西,但我不知道什么时候使用这些选项中的哪一个。 我目前正在开发基于 C# 的 Windows 服务,该服务需要定期验证来自不同数据源的不同数据组。实施验证本身对我来说并不是真正的问题,但我不确定如何同时处理所有验证 运行。 我需要一个解决方案,它允许我做以下所有事情:

  1. 运行 不同(预定义)时间间隔的验证。
  2. 从一个地方控制所有不同的验证,以便我可以暂停 and/or 在必要时停止它们,例如当用户停止或重新启动服务时。
  3. 尽可能高效地使用系统资源以避免性能问题。

到目前为止,我只有一个类似的项目,之前我只是简单地使用 Thread 对象与 ManualResetEventThread.Join 调用结合超时来通知线程当服务停止时。这些线程内部定期执行某些操作的逻辑如下所示:

while (!shutdownEvent.WaitOne(0))
{
    if (DateTime.Now > nextExecutionTime)
    {
        // Do something
        nextExecutionTime = nextExecutionTime.AddMinutes(interval);
    }

    Thread.Sleep(1000);
}

虽然这确实按预期工作,但我经常听说像这样直接使用线程被认为是“老派”甚至是一种不好的做法。我还认为这个解决方案不能非常有效地使用线程,因为它们大部分时间都在休眠。我怎样才能以更现代、更高效的方式实现这样的目标? 如果这个问题太模糊或基于意见,请告诉我,我会尽力使其尽可能具体。

您应该使用 Microsoft 的 Reactive Framework(又名 Rx)- NuGet System.Reactive 并添加 using System.Reactive.Linq; - 然后您可以这样做:

Subject<bool> starter = new Subject<bool>();

IObservable<Unit> query =
    starter
        .StartWith(true)
        .Select(x => x
            ? Observable.Interval(TimeSpan.FromSeconds(5.0)).SelectMany(y => Observable.Start(() => Validation()))
            : Observable.Never<Unit>())
        .Switch();
        
IDisposable subscription = query.Subscribe();

5.0 秒触发 Validation() 方法。

当您需要暂停和恢复时,请执行以下操作:

starter.OnNext(false);
// Now paused

starter.OnNext(true);
// Now restarted.

当你想停止这一切时调用subscription.Dispose()

问题感觉有点宽泛,但我们可以使用提供的代码并尝试改进它。

确实,现有代码的问题在于,在大多数情况下,它会在不执行任何有用操作(休眠)的情况下保持线程阻塞。此外,线程每秒唤醒一次只是为了检查间隔,并且在大多数情况下再次进入睡眠状态,因为它还不是验证时间。它为什么这样做?因为如果你要睡更长的时间——当你发出 shutdownEvent 信号然后加入一个线程时,你可能会阻塞很长时间。 Thread.Sleep 不提供根据请求中断的方法。

要解决这两个问题,我们可以使用:

  1. CancellationTokenSource+CancellationToken.

    形式的合作取消机制
  2. Task.Delay 而不是 Thread.Sleep.

例如:

async Task ValidationLoop(CancellationToken ct) {
    while (!ct.IsCancellationRequested) {
        try {
            var now = DateTime.Now;
            if (now >= _nextExecutionTime) {
                // do something
                _nextExecutionTime = _nextExecutionTime.AddMinutes(1);
            }

            var waitFor = _nextExecutionTime - now;
            if (waitFor.Ticks > 0) {
                await Task.Delay(waitFor, ct);
            }
        }
        catch (OperationCanceledException) {
            // expected, just exit
            // otherwise, let it go and handle cancelled task 
            // at the caller of this method (returned task will be cancelled).
            return;
        }
        catch (Exception) {
            // either have global exception handler here
            // or expect the task returned by this method to fail
            // and handle this condition at the caller
        }
    }
}

现在我们不再持有线程了,因为await Task.Delay不会这样做。而是在指定的时间间隔后,在一个空闲的线程池线程上执行后续代码(这个比较复杂,这里不赘述)。

我们也不需要无缘无故每秒醒来,因为Task.Delay接受取消令牌作为参数。当该令牌发出信号时 - Task.Delay 将立即因异常而中断,这是我们期望的并从验证循环中中断。

要停止提供的循环,您需要使用 CancellationTokenSource:

private readonly CancellationTokenSource _cts = new CancellationTokenSource();

然后将其 _cts.Token 标记传递给提供的方法。然后当你想给令牌发信号时,只需执行:

_cts.Cancel();

要进一步改进资源管理 - 如果您的验证代码使用任何 IO 操作(从磁盘、网络、数据库访问等读取文件)- 使用所述操作的 Async 个版本。然后在执行 IO 时,您将不会等待不必要的线程阻塞。

现在您不再需要自己管理线程,而是根据您需要执行的任务进行操作,让框架 OS 为您管理线程。