首页 > 解决方案 > 如何在基于 C# 的 Windows 服务中处理以不同时间间隔并行运行的多个任务?

问题描述

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

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

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

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

    Thread.Sleep(1000);
}

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

标签: c#multithreadingthread-safetytasktask-parallel-library

解决方案


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

事实上,现有代码的问题在于,在大多数情况下,它使线程处于阻塞状态,而没有做任何有用的事情(休眠)。此外,线程每秒唤醒一次只是为了检查间隔,并且在大多数情况下再次进入睡眠状态,因为它还不是验证时间。为什么这样做?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 时,您将不会保留任何不必要的线程阻塞等待。

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


推荐阅读