停止时服务偶尔会挂起:挂起的线程

Service occasionally hangs when stopping: suspended threads

我用 C# 编写了一个针对 .NET 4.0 的 Windows 服务,当我尝试停止该服务时,有时会完全挂起。通过查看转储文件,我注意到我的许多线程已挂起,但我自己并没有在我的代码中挂起它们。

环境是 Windows Server 2008R2 64 位,尽管我在 Windows 7 64 位上观察到相同的挂起。 .NET 4.0 是安装的最新版本。

有很多代码,所以我只是 post 一些希望相关的片段,如果需要,我可以 post 更多。

基本设计:

Main() 启动一个新线程来处理记录到文件(其代码在单独的 dll 中),然后启动服务。

public static void Main(string[] args)
{
    ...
    else if (Args.RunService)
    {
        Logger.Options.LogToFile = true;
        MSPO.Logging.Logger.Start();
        RunService();
        MSPO.Logging.Logger.Stop();
    }
    ...
}

private static void RunService()
{
    service = new ProcessThrottlerService();
    System.ServiceProcess.ServiceBase.Run(service);
}

该线程将保留在那里,直到 ServiceBase.Run returns.

服务中的 OnStart() 创建一个新线程并启动它。

protected override void OnStart(string[] args)
{
    serviceThread = new MainServiceThread();
    serviceThread.StartThread();
    base.OnStart(args);
}

我创建了一个 ManualResetEventSlim,用作程序其余部分的停止信号。 OnStop() 设置事件。

protected override void OnStop()
{
    if (serviceThread != null)
    {
        serviceThread.StopThread(); // Event is signalled in there
        serviceThread.WaitForThreadToReturn(); // This calls thread.Join() on the MainServiceThread thread
    }
    base.OnStop();
}

"MainServiceThread" 创建事件,再次启动新线程,然后等待事件。

private void StartHandlerAndWaitForServiceStop()
{
    processHandler.Start(serviceStopEvent);
    serviceStopEvent.Wait();
    processHandler.Stop();
}

processHandler 线程订阅此 WMI 查询:

watcher = new ManagementEventWatcher(new ManagementScope("root\CIMV2"),
    new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace"));
watcher.EventArrived += HandleNewProcessCreated;

如果对新进程名称感兴趣,我会创建一个新的 "throttler" 线程,它实际上只是挂起进程、休眠、恢复进程,然后再次休眠,在一个循环中:

while (true)
{
    ntresult = Ntdll.NtResumeProcess(processHandle);
    if (ntresult != Ntdll.NTSTATUS.STATUS_SUCCESS)
    {
        if (ntresult != Ntdll.NTSTATUS.STATUS_PROCESS_IS_TERMINATING)
            LogSuspendResumeFailure("resume", ntresult);
        break;
    }
    Thread.Sleep(resumeTime);

    ntresult = Ntdll.NtSuspendProcess(processHandle);
    if (ntresult != Ntdll.NTSTATUS.STATUS_SUCCESS)
    {
        if (ntresult != Ntdll.NTSTATUS.STATUS_PROCESS_IS_TERMINATING)
            LogSuspendResumeFailure("suspend", ntresult);
        break;
    }
    Thread.Sleep(suspendTime);

    if (++loop >= loopsBeforeCheckingStopEvent)
    {
        if (stopEvent.IsSet) break;
        loop = 0;
    }
}

如果服务收到停止命令,它将设置 ManualResetEventSlim 事件。任何线程 "throttling" 进程将在 1 秒内看到它并跳出 loop/return。进程处理程序线程将等待所有这些线程到 return,然后也是 return。那时 StartHandlerAndWaitForServiceStop() 方法 post 上面的 return 和其他一直在这里和那里等待的线程 return.

在我停止服务的绝大多数情况下,它都会毫无问题地停止。这与我是否有 0 或 500 个节流器线程 运行 无关,也与服务 运行.

时是否创建过节流器线程无关

然而,当我试图阻止它时(通过 services.msc),它会挂起。昨天,我设法在进程处于这种状态时创建了进程的完整转储。我使用 Process Explorer 创建了转储。

转储文件显示我的多个线程已挂起:

0:010> ~
   0  Id: 1840.c34 Suspend: 0 Teb: 000007ff`fffdd000 Unfrozen
   1  Id: 1840.548 Suspend: 0 Teb: 000007ff`fffdb000 Unfrozen
   2  Id: 1840.9c0 Suspend: 0 Teb: 000007ff`fffd9000 Unfrozen
   3  Id: 1840.1da8 Suspend: 0 Teb: 000007ff`fffd7000 Unfrozen
   4  Id: 1840.b08 Suspend: 3 Teb: 000007ff`fffd5000 Unfrozen
   5  Id: 1840.1b5c Suspend: 0 Teb: 000007ff`ffef6000 Unfrozen
   6  Id: 1840.af0 Suspend: 2 Teb: 000007ff`ffef2000 Unfrozen
   7  Id: 1840.c60 Suspend: 0 Teb: 000007ff`ffef0000 Unfrozen
   8  Id: 1840.1d94 Suspend: 4 Teb: 000007ff`ffeee000 Unfrozen
   9  Id: 1840.1cd8 Suspend: 4 Teb: 000007ff`ffeec000 Unfrozen
. 10  Id: 1840.1c64 Suspend: 0 Teb: 000007ff`ffefa000 Unfrozen
  11  Id: 1840.1dc8 Suspend: 0 Teb: 000007ff`fffd3000 Unfrozen
  12  Id: 1840.8f4 Suspend: 0 Teb: 000007ff`ffefe000 Unfrozen

这与我在 Process Explorer 中看到的有关 - 在我 "throttling" 的两个进程中,一个被永久挂起,另一个被永久恢复。因此,这些节流器线程实际上被挂起,因为它们不再工作。他们应该不可能不被暂停就停止,因为我有错误处理围绕它,任何异常都会导致这些线程记录信息和 return。此外,他们的调用堆栈没有显示任何错误。由于某些错误,他们没有永久休眠,因为两次休眠的休眠时间分别为 22 和 78 毫秒,在我尝试停止服务之前它工作正常。

所以我试图了解这些线程是如何暂停的。我唯一的怀疑是 GC,因为在 reclaiming/compacting 内存时挂起线程。

我在这里粘贴了 !eestack 和 ~*kb 的内容:http://pastebin.com/rfQK0Ak8

我应该提到我没有符号,因为在创建转储时我已经多次重建应用程序。但是,由于它是 .NET,我想这不是什么大问题?

来自 eestack,我认为这些是 "my" 个线程:

就是这样,根据转储文件,线程 4、6、8 和 9 被挂起。因此,除了主线程和处理 OnStop() 方法的线程外,所有 "my" 个线程都被挂起。

现在我不太了解 GC 和调试 .NET 的东西,但线程 10 对我来说看起来很狡猾。调用堆栈摘录:

Thread  10
Current frame: ntdll!NtWaitForMultipleObjects+0xa
Child-SP         RetAddr          Caller, Callee
000000001a83d670 000007fefdd41420 KERNELBASE!WaitForMultipleObjectsEx+0xe8, calling ntdll!NtWaitForMultipleObjects
000000001a83d6a0 000007fef4dc3d7c clr!CExecutionEngine::ClrVirtualAlloc+0x3c, calling kernel32!VirtualAllocStub
000000001a83d700 000007fefdd419bc KERNELBASE!WaitForMultipleObjectsEx+0x224, calling ntdll!RtlActivateActivationContextUnsafeFast
000000001a83d710 000007fef4e9d3aa clr!WKS::gc_heap::grow_heap_segment+0xca, calling clr!StressLog::LogOn
000000001a83d730 000007fef4e9cc98 clr!WKS::gc_heap::adjust_limit_clr+0xec, calling clr!memset
000000001a83d740 000007fef4df398d clr!COMNumber::FormatInt32+0x8d, calling clr!LazyMachStateCaptureState
000000001a83d750 000007fef4df398d clr!COMNumber::FormatInt32+0x8d, calling clr!LazyMachStateCaptureState
000000001a83d770 00000000778a16d3 kernel32!WaitForMultipleObjectsExImplementation+0xb3, calling kernel32!WaitForMultipleObjectsEx
000000001a83d7d0 000007fef4e9ce73 clr!WKS::gc_heap::allocate_small+0x158, calling clr!WKS::gc_heap::a_fit_segment_end_p
000000001a83d800 000007fef4f8f8e1 clr!WaitForMultipleObjectsEx_SO_TOLERANT+0x91, calling kernel32!WaitForMultipleObjectsExImplementation
000000001a83d830 000007fef4dfb798 clr!Thread::GetApartment+0x34, calling clr!GetThread
000000001a83d860 000007fef4f8f6ed clr!Thread::GetFinalApartment+0x1a, calling clr!Thread::GetApartment
000000001a83d890 000007fef4f8f6ba clr!Thread::DoAppropriateAptStateWait+0x56, calling clr!WaitForMultipleObjectsEx_SO_TOLERANT
000000001a83d8d0 000007fef4f8f545 clr!Thread::DoAppropriateWaitWorker+0x1b1, calling clr!Thread::DoAppropriateAptStateWait
000000001a83d990 000007fef4ecf167 clr!ObjectNative::Pulse+0x147, calling clr!HelperMethodFrameRestoreState
000000001a83d9d0 000007fef4f8f63b clr!Thread::DoAppropriateWait+0x73, calling clr!Thread::DoAppropriateWaitWorker
000000001a83da50 000007fef4f0ff6a clr!Thread::JoinEx+0xa6, calling clr!Thread::DoAppropriateWait
000000001a83dac0 000007fef4defd90 clr!GCHolderBase<0,0,0,0>::EnterInternal+0x3c, calling clr!Thread::EnablePreemptiveGC
000000001a83daf0 000007fef4f1039a clr!ThreadNative::DoJoin+0xd8, calling clr!Thread::JoinEx
000000001a83db20 000007fef45f86f3 (MethodDesc 000007fef3cbe8d8 +0x1a3 System.Threading.SemaphoreSlim.Release(Int32)), calling 000007fef4dc31b0 (stub for System.Threading.Monitor.Exit(System.Object))
000000001a83db60 000007fef4dfb2a6 clr!FrameWithCookie<HelperMethodFrame_1OBJ>::FrameWithCookie<HelperMethodFrame_1OBJ>+0x36, calling clr!GetThread
000000001a83db90 000007fef4f1024d clr!ThreadNative::Join+0xfd, calling clr!ThreadNative::DoJoin
000000001a83dc40 000007ff001723f5 (MethodDesc 000007ff001612c0 +0x85 MSPO.Logging.MessageQueue.EnqueueMessage(System.String)), calling (MethodDesc 000007fef30fde88 +0 System.Collections.Concurrent.BlockingCollection`1[[System.__Canon, mscorlib]].TryAddWithNoTimeValidation(System.__Canon, Int32, System.Threading.CancellationToken))
000000001a83dcf0 000007ff001720e9 (MethodDesc 000007ff00044bb0 +0xc9 ProcessThrottler.Logging.Logger.Log(LogLevel, System.String)), calling (MethodDesc 000007ff00161178 +0 MSPO.Logging.MessageFormatter.QueueFormattedOutput(System.String, System.String))
000000001a83dd10 000007fef4f101aa clr!ThreadNative::Join+0x5a, calling clr!LazyMachStateCaptureState
000000001a83dd30 000007ff0018000b (MethodDesc 000007ff00163e10 +0x3b ProcessThrottler.Service.MainServiceThread.WaitForThreadToReturn()), calling 000007fef4f10150 (stub for System.Threading.Thread.JoinInternal())
000000001a83dd60 000007ff0017ff44 (MethodDesc 000007ff00049f30 +0xc4 ProcessThrottler.Service.ProcessThrottlerService.OnStop()), calling 000007ff0004d278 (stub for ProcessThrottler.Service.MainServiceThread.WaitForThreadToReturn())
000000001a83dda0 000007fef63fcefb (MethodDesc 000007fef63d65e0 +0xbb System.ServiceProcess.ServiceBase.DeferredStop())

我可以 post 更多代码显示我的每个函数在做什么,但我真的不认为这是我的代码中的死锁,因为在那种情况下线程不会暂停。所以我正在查看上面的调用堆栈,并在我告诉它将字符串记录到队列后看到它正在执行一些 GC 操作。但是 none 的 GC 东西看起来很狡猾,至少与我在 http://blogs.msdn.com/b/tess/archive/2008/02/11/hang-caused-by-gc-xml-deadlock.aspx 中看到的相比没有 我有一个配置文件告诉它使用 gcServer,但我几乎可以肯定它没有使用该设置是因为在我之前的测试中 GCSettings.IsServerGC 总是 returned false.

所以...有人对我的线程被暂停的原因有任何建议吗?

顺便说一句,这是我的 OpenProcess 方法,它获取进程的句柄 suspended/resumed,以回应 Hans 的评论:

private void GetProcessHandle(CurrentProcessDetails process)
{
    IntPtr handle = Kernel32.OpenProcess(
        process.Settings.RequiredProcessAccessRights,
        false,
        (uint)process.ID
        );
    if (handle == IntPtr.Zero)
        throw new Win32ExceptionWrapper(
            string.Format("Failed to open process {0} {1}", 
            process.Settings.ProcessNameWithExt, process.IDString));
    process.Handle = handle;
}

我已经找到原因了。它与我的代码无关。这是 Process Explorer 中的错误。

我的程序是针对 .NET 4.0 编写的。如果我使用 Process Explorer 查看我的任何线程的调用堆栈,Process Explorer 会挂起线程并且不会恢复它。它应该做的是在获取调用堆栈时挂起线程,然后立即恢复。但它不会恢复线程 - 无论如何都不会恢复我的托管线程。

我可以用这个非常简单的代码复制它:

using System;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < int.MaxValue; i++)
            {
                Console.WriteLine(i.ToString());
            }
        }   
    }
}

如果我将其编译为目标 .NET 4.0 或更高版本,运行 它,并使用 Process Explorer 打开线程 运行 循环,线程将被挂起。恢复按钮将可用,我可以单击它恢复线程。多次开启线程导致多次挂起;我通过使用 Windbg 查看线程的挂起计数来确认这一点。

如果我将它编译为低于 4.0 的目标版本(尝试过 2.0 和 3.5),我在 Process Explorer 中打开的线程不会保持暂停状态。