【问题标题】:How to figure out who owns a worker thread that is still running when my app exits?如何确定谁拥有我的应用程序退出时仍在运行的工作线程?
【发布时间】:2010-12-21 01:09:32
【问题描述】:

升级到 VS2010 后不久,我的应用程序无法正常关闭。如果我关闭应用程序然后在 IDE 中点击暂停,我会看到:

问题是,没有上下文。调用堆栈只是说[外部代码],这并没有太大帮助。

这是我迄今为止为缩小问题范围所做的工作:

  • 删除了所有无关的插件,以尽量减少启动的工作线程数
  • 在我创建工作线程的任何地方(以及委托+ BeginInvoke,因为我认为它们在调试器中被标记为“工作线程”)在我的代码中设置断点。没有人被击中。
  • 为所有线程设置 IsBackground = true

虽然我可以执行下一个蛮力步骤,即将我的代码回滚到该没有发生的点,然后查看所有更改日志,但这不是非常高效。鉴于调试器提供的信息明显缺乏,任何人都可以推荐一种更好的方法来解决这个问题吗?

我能想到的唯一其他事情包括:

  • 阅读 WinDbg 并尝试在线程启动时使用它来停止。至少,我认为这是可能的...... :)
  • 注释掉大量代码,直到应用正常关闭,然后开始取消注释,直到它关闭为止。

更新

也许这些信息会有用。我决定使用 WinDbg 并附加到我的应用程序。然后我关闭它,切换到线程 0 并转储堆栈内容。这是我所拥有的:

ThreadCount:      6
UnstartedThread:  0
BackgroundThread: 1
PendingThread:    0
DeadThread:       4
Hosted Runtime:   no
                                   PreEmptive   GC Alloc                Lock
       ID  OSID ThreadOBJ    State GC           Context       Domain   Count APT Exception
   0    1  1c70 005a65c8      6020 Enabled  02dac6e0:02dad7f8 005a03c0     0 STA
   2    2  1b20 005b1980      b220 Enabled  00000000:00000000 005a03c0     0 MTA (Finalizer)
XXXX    3       08504048     19820 Enabled  00000000:00000000 005a03c0     0 Ukn
XXXX    4       08504540     19820 Enabled  00000000:00000000 005a03c0     0 Ukn
XXXX    5       08516a90     19820 Enabled  00000000:00000000 005a03c0     0 Ukn
XXXX    6       08517260     19820 Enabled  00000000:00000000 005a03c0     0 Ukn
0:008> ~0s
eax=c0674960 ebx=00000000 ecx=00000000 edx=00000000 esi=0040f320 edi=005a65c8
eip=76c37e47 esp=0040f23c ebp=0040f258 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
USER32!NtUserGetMessage+0x15:
76c37e47 83c404          add     esp,4
0:000> !clrstack
OS Thread Id: 0x1c70 (0)
Child SP IP       Call Site
0040f274 76c37e47 [InlinedCallFrame: 0040f274] 
0040f270 6baa8976 DomainBoundILStubClass.IL_STUB_PInvoke(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\WindowsBase\d17606e813f01376bd0def23726ecc62\WindowsBase.ni.dll

0040f274 6ba924c5 [InlinedCallFrame: 0040f274] MS.Win32.UnsafeNativeMethods.IntGetMessageW(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
0040f2c4 6ba924c5 MS.Win32.UnsafeNativeMethods.GetMessageW(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
0040f2dc 6ba8e5f8 System.Windows.Threading.Dispatcher.GetMessage(System.Windows.Interop.MSG ByRef, IntPtr, Int32, Int32)
0040f318 6ba8d579 System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame)
0040f368 6ba8d2a1 System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame)
0040f374 6ba7fba0 System.Windows.Threading.Dispatcher.Run()
0040f380 62e6ccbb System.Windows.Application.RunDispatcher(System.Object)*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v4.0.30319_32\PresentationFramewo#\7f91eecda3ff7ce478146b6458580c98\PresentationFramework.ni.dll

0040f38c 62e6c8ff System.Windows.Application.RunInternal(System.Windows.Window)
0040f3b0 62e6c682 System.Windows.Application.Run(System.Windows.Window)
0040f3c0 62e6c30b System.Windows.Application.Run()
0040f3cc 001f00bc MyApplication.App.Main() [C:\code\trunk\MyApplication\obj\Debug\GeneratedInternalTypeHelper.g.cs @ 24]
0040f608 66c421db [GCFrame: 0040f608]

编辑——不确定这是否有帮助,但主线程的调用堆栈如下所示:

    [Managed to Native Transition]  
>   WindowsBase.dll!MS.Win32.UnsafeNativeMethods.GetMessageW(ref System.Windows.Interop.MSG msg, System.Runtime.InteropServices.HandleRef hWnd, int uMsgFilterMin, int uMsgFilterMax) + 0x15 bytes  
    WindowsBase.dll!System.Windows.Threading.Dispatcher.GetMessage(ref System.Windows.Interop.MSG msg, System.IntPtr hwnd, int minMessage, int maxMessage) + 0x48 bytes 
    WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame frame = {System.Windows.Threading.DispatcherFrame}) + 0x85 bytes 
    WindowsBase.dll!System.Windows.Threading.Dispatcher.PushFrame(System.Windows.Threading.DispatcherFrame frame) + 0x49 bytes  
    WindowsBase.dll!System.Windows.Threading.Dispatcher.Run() + 0x4c bytes  
    PresentationFramework.dll!System.Windows.Application.RunDispatcher(object ignore) + 0x17 bytes  
    PresentationFramework.dll!System.Windows.Application.RunInternal(System.Windows.Window window) + 0x6f bytes 
    PresentationFramework.dll!System.Windows.Application.Run(System.Windows.Window window) + 0x26 bytes 
    PresentationFramework.dll!System.Windows.Application.Run() + 0x1b bytes 

我搜索了一下,发现一些与 WPF GUI 相关的帖子挂了,也许这会给我更多的线索。

【问题讨论】:

  • 你的主线程在做什么?
  • 只是坐在那里等着有人点击按钮。
  • @Dave 我的意思是当你“关闭”应用程序时它在做什么?它是否正在执行一些关闭事件处理程序并等待永远不会发生的事情?
  • 这是我想不通的。其实我之前说错了。当我尝试拉起主线程的调用堆栈时,我得到[External code]。当我为工作线程执行此操作时,调用堆栈完全为空。
  • @Dave 当你切换到线程 1 即你的主线程时你会得到什么转储?

标签: c# wpf multithreading visual-studio-2010 .net-4.0


【解决方案1】:

将以下处理程序添加到应用程序在单独线程中创建的每个窗口:

win.Closed += (o, e) => win.Dispatcher.InvokeShutdown();

如果主线程挂起,请在MainWindow.Closed 中调用win.Dispatcher.InvokeShutdown() - 这将自动关闭在主线程中创建的所有其他窗口。

如果没有这个处理程序,我的应用程序也会在退出时挂起以下代码:

void Worker() {
    var win = new Window();
    // win.Closed += onWindowClose ?? ((o, e) => editor.Dispatcher.InvokeShutdown());
    editor.Show();
    System.Windows.Threading.Dispatcher.Run();
}

【讨论】:

  • 感谢您的建议,我会将此添加为预防措施,但我的应用程序只有主窗口。但你永远不知道......我会让你知道它是怎么回事。 :)
  • +1,这解决了我遇到的一个相关的烦人问题。第三方组件在关闭时挂起应用程序(我认为它一定是在创建某种隐藏窗口)并从主窗口关闭事件中调用它来修复它。
【解决方案2】:

您看到的工作线程 ID 为 0。这是一个框架线程并且是预期的 - 它不是程序“生成”的线程。如果您附加到任何 .Net 进程,您将看到这一点。我不确定它是哪个框架线程 - 绝对不是终结器线程,因为它永远不是线程 0。也许是 JIT 线程?

然后更有趣的是您的主线程,因为它似乎挂起。我会专注于调试你的主线程以解决这个问题。例如,它是否因窗口关闭事件处理程序而死锁,等待永远不会发生的事情?

更新

在阅读了添加到问题中的主线程的堆栈跟踪之后,运行一个测试以确定主线程是否停止或者它是否只是空闲等待消息(并且它正在等待对于它从未收到或从未发送过的 WM_CLOSE)。

以下代码可用于手动向您的应用程序发送 WM_CLOSE 消息。只需等待程序在关闭后挂起,然后运行代码。将进程名称替换为您自己的名称。

更新 2

好的,看起来主线程确实挂起,因为它没有处理 WM_CLOSE 或 WM_QUIT 消息。

请尝试制作可以重现问题的最小应用程序并发布代码。

WM_CLOSE\WM_QUIT 应用示例

internal class Program
{
    private const int WM_QUIT = 0x0012;
    private const int WM_CLOSE = 0x0010;

    [DllImport("user32.dll")]
    private static extern bool PostMessage(int hhwnd, uint msg, IntPtr wParam, IntPtr lParam);

    private static void Main()
    {
        Process p = GetProcess("Your process name - no '.exe' required");

        CloseMainWindow(p);
    }

    private static Process GetProcess(string name)
    {
        List<Process> processes = Process.GetProcessesByName(name).ToList();

        if (processes.Count != 1)
        {
            throw new Exception(
              "Expected 1 process with name '" + name +
              "' but found " + processes.Count + ".");
        }

        return processes[0];
    }

    private static void CloseMainWindow(Process p)
    {
        PostMessage(p, WM_CLOSE, "close");
    }

    private static void QuitApplication(Process p)
    {
        PostMessage(p, WM_QUIT, "quit");
    }

    private static void PostMessage(Process p, uint message, string name)
    {
        Console.WriteLine("Posting {0} message to '{1}'...", name, p.ProcessName);

        bool succeeded = PostMessage(p.MainWindowHandle.ToInt32(), message, IntPtr.Zero, IntPtr.Zero);

        Console.WriteLine("Posted {0} message to '{1}' (succeeded:{2}).", name, p.ProcessName, succeeded);
    }
} 

【讨论】:

  • @chibacity:我明白你在说什么——托管 ID 为 0,这就是 Thread.CurrentThread.GetHashCode() 返回的内容,不是吗?嗯...我会看看你的建议,谢谢。
  • @Dave 托管线程 id 是由框架管理的,可以通过“Thread.CurrentThread.ManagedThreadId”检索。它与由操作系统管理的本机 id 不同。托管 id 为 0 的线程是框架线程,而不是您已生成或直接使用的线程。这是框架管理的第一个线程,因此它的 id 为 0。它位于 id 为 1 的主线程之前,位于 2 的终结器线程之前。
  • @chibacity 托管线程 ID 0 来来去去有意义吗?只是为了测试一下,我暂停了应用程序的执行并检查了正在运行的线程。我会打开各种对话框,暂停,检查线程,取消暂停。有时托管线程 ID 0 存在,有时它消失了,有时又回来了。这意味着什么?我正在尝试在网上查找可以解释线程 0 重要性的资源。
  • @Dave 我不知道框架线程如何详细表现的血腥细节。关于此在线的信息也很少 - 它非常内部,因此没有广泛发布\记录。不过,我仍然认为您的主线程有问题,并且您正在查看线程 0 的错误树。如果没有源代码,我真的无能为力!你不能做一个小的可重复的样品吗?如果你不能,这是更有力的证据,证明它是你的主线程,即程序特定的。
  • @chibacity 谢谢——抱歉,我搞混了,想知道你的答案更新在哪里。 :) 傻我。我会看看这个。
【解决方案3】:

我终于想通了这个问题。我有一个由 MEF 编写的 Imported 控件,但实际上从未调用过(还)。我认为 MEF 实例化了它,即使它没有在任何地方引用(我假设创建没有发生直到请求资源,但显然我错了)。我通过使用 Lazy 实例化解决了这个问题,现在它可以工作了。这个真的把我扔了,但感谢大家的帮助。我在尝试调试这个问题时学到了很多东西。

【讨论】:

    【解决方案4】:

    如果您正在创建工作线程(而不是池线程),请将它们的 Name 设置为创建时的描述性内容。

    【讨论】:

    • 如果你真的陷入困境,你可以使用反射重命名池线程。如果您对 course 感到绝望...
    • 这是对未来的一个很好的建议,但我确实在我创建线程的 每个 位置设置了断点(所以如果我在那里错过它,我会错过名称为好 :) )。是否有其他地方可以在创建任何线程时设置断点,例如 IDE 设置?
    • @Dave 当您使用 BeginInvoke 等构造时,您实际上并没有创建线程。工作项在线程池中排队,现有线程将其拾取。
    • 好的,谢谢。我知道 BeginInvoke 使用 ThreadPool,但我认为它们仍然以 WorkerThread 的形式出现在 Threads 窗口中。我猜不会?我得看一看……我知道它们就在某个地方,因为我在单步执行我的代码时看到了它们。
    • @Mitch 感谢您的建议。虽然它不能解决我的问题,但我真的很喜欢查看“线程”窗口,现在看到的东西比“工作线程”更有意义。 :) 好多了!
    【解决方案5】:

    将日志记录添加到所有代码以记录方法和线程 ID。您可以使用正则表达式替换将其放在每个方法的开头。然后运行应用程序并关闭。查看哪些日志消息与未关闭的工作线程具有相同的线程 ID。

    【讨论】:

    • 工作线程 ID 为 0 - 它是一个框架线程,虽然我不确定它的作用是什么。我认为这实际上是主线程的问题。
    • 谢谢。我认为这是迄今为止最好的建议。我已经用 log4net 记录了所有这些信息,我什至没有想到要通过日志跟踪线程行为以这种方式获取上下文!好的。 :)
    • 有趣的是,日志在任何地方都没有显示 3256,但 6524 在那里。此时应用程序执行看起来完全正常,并且在我关闭之前运行良好。我将尝试更改 log4net 配置以记录所有入口点,我们将看看是否有任何结果。
    • 托管线程Id为0,不是native id。这是由框架管理的第一个线程,因此它的 id 为 0。它位于 id 为 1 的主线程之前,并且在为 2 的终结器线程之前。托管线程 id 由框架,并且与您引用的由操作系统管理的本机 id 不同。由于它是一个专用的框架线程,因此它并没有被程序“生成”。它在程序之前启动(即在 Main 之前)并由框架使用。所有 .Net 程序都有这个线程。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-27
    • 1970-01-01
    • 2013-07-17
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    相关资源
    最近更新 更多