如何正确停止多线程 .NET Windows 服务?

Posted

技术标签:

【中文标题】如何正确停止多线程 .NET Windows 服务?【英文标题】:How to properly stop a multi-threaded .NET windows service? 【发布时间】:2010-12-04 10:24:34 【问题描述】:

我有一个用 C# 编写的 Windows 服务,它创建大量线程并建立许多网络连接(WMI、SNMP、简单 TCP、http)。尝试使用 Services MSC 管理单元停止 Windows 服务时,停止服务的调用返回相对较快,但该进程继续运行大约 30 秒左右。

主要问题是停止需要 30 多秒的原因可能是什么。我可以寻找什么以及如何寻找它?

第二个问题是为什么即使进程仍在运行,服务 msc 管理单元(服务控制器)仍会返回。有没有办法让它只在进程实际被杀死时才返回?

这是服务的 OnStop 方法中的代码

protected override void OnStop()

   //doing some tracing
   //......

   //doing some minor single threaded cleanup here
   //......

   base.OnStop();

   //doing some tracing here

编辑以响应线程清理答案

你们中的许多人回答说我应该跟踪我的所有线程然后清理它们。我不认为这是一种实用的方法。首先,我无法访问一个位置的所有托管线程。该软件非常大,包含不同的组件、项目,甚至可以创建线程的 3rd 方 dll。我无法在一个位置跟踪所有这些,或者有一个所有线程都检查的标志(即使我可以让所有线程检查一个标志,许多线程也会阻塞信号量之类的东西。当它们阻塞时,它们可以'不检查。我将不得不让他们等待超时,然后检查这个全局标志并再次等待)。

IsBackround 标志是一个有趣的检查。尽管如此,我怎样才能知道我是否有任何前台线程在运行?我将不得不检查创建线程的代码的每个部分。有没有其他方法,也许有一个工具可以帮助我找到这个。

但最终,该过程确实停止了。似乎我只需要等待一些东西。但是,如果我在 OnStop 方法中等待 X 时间,则该过程大约需要 30 秒 + X 才能停止。无论我尝试做什么,在 OnStop 返回后,该过程似乎需要大约 30 秒(并不总是 30 秒,它可能会有所不同)才能真正停止。

【问题讨论】:

你有没有放任何东西让其他线程适当地停止?它们是后台线程还是前台线程? 如果您使用可以在内部创建线程的组件,理想情况下,它们各自都会公开一个您可以在 OnStop 中调用的适当关闭机制,因此您不必直接管理它们的线程。如果没有,或者如果您不想打扰干净退出并且只想让进程立即终止,请尝试调用 Environment.Exit... 但是我不确定当服务终止时 SCM 将如何反应向它发送停止命令。 【参考方案1】:

一旦您的OnStop() 回调返回,停止服务的调用就会返回。根据您所展示的内容,您的 OnStop() 方法作用不大,这就解释了为什么它返回得如此之快。

有几种方法可以让您的服务退出。

首先,您可以重新编写OnStop() 方法以通知所有线程关闭并等待它们关闭后再退出。正如@DSO 建议的那样,您可以使用全局布尔标志来执行此操作(确保将其标记为volatile)。我通常使用 ManualResetEvent,但两者都可以。通知线程退出。然后加入具有某种超时期限的线程(我通常使用 3000 毫秒)。如果此时线程还没有退出,可以调用Abort()方法退出。通常,Abort() 方法是不受欢迎的,但考虑到您的进程无论如何都会退出,这没什么大不了的。如果您始终有一个必须中止的线程,您可以重新设计该线程以更好地响应您的关闭信号。

其次,将您的线程标记为background 线程(有关详细信息,请参阅here)。听起来您正在为线程使用 System.Threading.Thread 类,默认情况下它们是前台线程。这样做将确保线程不会阻止进程退出。如果您只执行托管代码,这将正常工作。如果您有一个线程正在等待非托管代码,我不确定设置 IsBackground 属性是否仍会导致线程在关闭时自动退出,即,您可能仍然需要修改线程模型以使该线程响应您的关闭请求。

【讨论】:

我接受了这个答案,因为它提到了 IsBackground 线程属性。这是我唯一需要改变的。我不相信创建一个任何和每个组件都应该使用的全局标志——在我看来,这太耦合了。但是,如果线程被正确标记为后台线程,则服务会正常停止。 我也不会使用全局标志/事件。我所做的是围绕 System.Threading.Thread 对象创建了一个包装器。此包装器的构造函数创建线程、设置名称并设置 IsBackground 属性。我有公共方法来启动和停止线程。尤其是 Stop() 方法,它设置了一个私有 ManualResetEvent 信号,通知线程停止运行。为了使其完全灵活,构造函数接受相当于 System.Threading.ThreadStart 委托的内容,允许任何人使用此类而无需从它继承。【参考方案2】:

当您从 OnStop 返回时,服务控制管理器 (SCM) 将返回。所以你需要修复你的 OnStop 实现以阻塞直到所有线程都完成。

一般的方法是让 OnStop 发出所有线程停止的信号,然后等待它们停止。为避免无限期阻塞,您可以给线程一个时间限制来停止,如果它们花费的时间太长,则中止它们。

这是我过去所做的:

    创建一个名为的全局布尔标志 停止,服务时设置为false 已启动。 调用 OnStop 方法时,将 Stop 标志设置为 true,然后对所有未完成的工作线程执行 Thread.Join。 每个工作线程负责检查停止标志,并在它为真时干净地退出。此检查应经常进行,并且始终在长时间运行的操作之前进行,以避免它延迟服务关闭太久。 在 OnStop 方法中,Join 调用也有一个超时,以给线程一个有限的时间来干净地退出...之后您只需中止它。

注意#4 你应该给你的线程在正常情况下退出的足够时间。中止应该只在线程挂起的异常情况下发生......在这种情况下,中止并不比用户或系统终止进程(后者如果计算机正在关闭)更糟糕。

【讨论】:

+1,除非您知道所有组件在做什么,并且有办法加入(和终止)它们在不同线程上执行的长时间运行的操作,否则无法解决此问题。您可以通过在其等待操作上设置超时、在循环内执行等待以及检查您的关闭标志作为循环的退出条件来处理阻塞信号量。【参考方案3】:

执行此操作的简单方法如下所示: -首先创建一个全球事件

ManualResetEvent shutdownEvent;
-at service start 创建手动重置事件并将其设置为未发出信号的初始状态
shutdownEvent = new ManualResetEvent(false);

-at 服务停止事件

shutdownEvent.Set();
别忘了等待线程结束
做

 //向服务管理器发送消息以获得更多时间
 //控制等待线程停止的时间

而(not_all_threads_stopped);

-每个线程必须时时测试,事件停止

if (shutdownEvent.WaitOne(delay, true)) 中断;

【讨论】:

【参考方案4】:

发出你的线程循环退出的信号,把它清理干净,然后做线程连接。看看它需要多长时间作为问题所在的度量/秒表。避免因各种原因中止关机..

【讨论】:

【参考方案5】:

回答第一个问题(为什么服务会持续运行 30+ 秒): 有很多原因。例如,在使用 WCF 时,停止 Host 会导致进程停止接受传入请求,并在停止之前等待处理所有当前请求。

对于可能其他类型的网络操作也是如此:操作将在终止之前尝试完成。这就是为什么大多数网络请求在请求可能“挂起”(服务器宕机、网络问题等)时都有一个内置的超时值。

如果没有更多关于你在做什么的信息,就无法具体告诉你为什么需要 30 秒,但这可能是超时。

回答第二个问题(为什么服务控制器返回):我不确定。我知道 ServiceController 类有一个 WaitForState 方法,该方法允许您等到达到给定状态。服务控制器可能正在等待预定时间(另一个超时),然后强行终止您的应用程序。

也很有可能是base.OnStop方法被调用了,OnStop方法又返回了,通知ServiceController进程已经停止,而实际上还有一些线程没有停止。您负责终止这些线程。

【讨论】:

【参考方案6】:

对于像我一样寻求缩短关闭时间的解决方案的人,请尝试设置 ServiceHost 的 CloseTimeout。

现在我试图理解为什么没有它需要这么长时间才能停止,我也认为这是线程问题。我确实查看了 Visual Studio,附加到服务并停止它:我的服务启动了一些仍在运行的线程。

现在的问题是:真的是这些线程让我的服务停止如此缓慢吗?微软没有考虑过吗?您不认为这可能是端口释放问题或其他问题吗?因为处理线程sto和最后没有更短的关闭时间是浪费时间。

【讨论】:

【参考方案7】:

马特戴维斯非常完整。 几点; 如果你有一个永远运行的线程(因为它有一个近乎无限的循环和一个包罗万象的线程)并且你的 service 的工作是运行那个线程,你可能希望它是一个前台线程。

此外,如果您的任何任务正在执行更长的操作,例如存储过程调用,因此您的加入超时需要更长一点,您实际上可以要求 SCM 有更多时间来关闭。见:https://msdn.microsoft.com/en-us/library/system.serviceprocess.servicebase.requestadditionaltime(v=vs.110).aspx 这对于避免可怕的“标记为删除”状态很有用。最大值是在注册表中设置的,所以我通常会要求线程通常关闭的最大预期时间(并且永远不会超过 12 秒)。见:what is the maximum time windows service wait to process stop request and how to request for additional time

我的代码如下所示:

private Thread _worker;       
private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 

protected override void OnStart(string[] args)

    _worker = new Thread(() => ProcessBatch(_cts.Token));
    _worker.Start();             


protected override void OnStop()
            
    RequestAdditionalTime(4000);
    _cts.Cancel();            
    if(_worker != null && _worker.IsAlive)
        if(!_worker.Join(3000))
            _worker.Abort(); 


private void ProcessBatch(CancellationToken cancelToken)

   while (true)
   
       try
       
           if(cancelToken.IsCancellationRequested)
                return;               
           // Do work
           if(cancelToken.IsCancellationRequested)
                return;
           // Do more work
           if(cancelToken.IsCancellationRequested)
                return;
           // Do even more work
       
       catch(Exception ex)
       
           // Log it
       
   

【讨论】:

以上是关于如何正确停止多线程 .NET Windows 服务?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确暂停/停止线程?

停止线程池的正确方法

3.线程的八大核心基础知识之如何正确停止线程

如何正确停止线程

如何正确停止线程

C++/Windows 多线程同步/数据共享