Как выяснить, кому принадлежит рабочий поток, который все еще работает, когда мое приложение выходит?

Вскоре после обновления до 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, и, возможно, это даст мне еще несколько подсказок.


person Dave    schedule 21.12.2010    source источник
comment
Что делает ваш основной поток?   -  person Tim Lloyd    schedule 21.12.2010
comment
Просто сижу и жду, пока кто-нибудь нажмет кнопку.   -  person Dave    schedule 21.12.2010
comment
@ Дэйв, я имею в виду, что он делает, когда вы закрыли приложение? Выполняется ли какой-то обработчик событий закрытия и ждет ли чего-то, что никогда не произойдет?   -  person Tim Lloyd    schedule 21.12.2010
comment
вот что я не могу понять. Я действительно оговорился ранее. Когда я пытаюсь подтянуть стек вызовов для основного потока, я получаю [External code]. Когда я делаю это для рабочего потока, стек вызовов полностью пуст.   -  person Dave    schedule 21.12.2010
comment
@Dave А какой дамп вы получаете, когда переключаетесь на поток 1, то есть на ваш основной поток?   -  person Tim Lloyd    schedule 21.12.2010
comment
@chibacity: мне потребовалось время, чтобы вернуться к этому. Нет нити 1 (см. выше). Похоже, 0 должен быть основным потоком...   -  person Dave    schedule 22.12.2010
comment
@Dave Дэйв, я бы сосредоточился на создании небольшого приложения, чтобы попытаться воспроизвести это. В частности, я бы обратил внимание на код, связанный с тем, как отображается главное окно и как пользователь взаимодействует с приложением, чтобы закрыть его, а также на код, который подключается к этому, т. е. обработчики событий и т. д. Создание скелета запуска и завершения работы. код, имитирующий ваше приложение, будет хорошим началом.   -  person Tim Lloyd    schedule 23.12.2010
comment
@Dave Трассировка стека вашего основного потока указывает на то, что ваш основной поток все еще выполняет цикл обработки сообщений и бездействует, ожидая сообщения, то есть GetMessageW. На самом деле закрытие окна должно привести к получению сообщения WM_QUIT, чтению цикла сообщений, а затем выходу и раскручиванию основного потока, т.е. нормальному выходу приложения. По какой-то причине он не получил WM_QUIT.   -  person Tim Lloyd    schedule 23.12.2010
comment
@Dave Или он не получил сообщение WM_CLOSE для главного окна.   -  person Tim Lloyd    schedule 23.12.2010
comment
@chibacity Хорошо, я посмотрю на это. Приложение довольно большое, но я постараюсь сузить его. Я не видел этой проблемы до тех пор, пока не преобразовал все в .NET 4.0 для VS2010... может быть связано с чем-то, что я сделал во время преобразования.   -  person Dave    schedule 23.12.2010
comment
@Dave Сначала попробуйте программу WM_CLOSE в моем обновлении ответа - по крайней мере, тогда мы будем знать, действительно ли основной поток зависает.   -  person Tim Lloyd    schedule 23.12.2010
comment
@chibacity не уверен, могу ли я использовать Wspector в приложениях WPF, но я создал обработчик для события Closed моего основного графического интерфейса, и он обрабатывается. Я предполагаю, что это означает, что сообщение WM_CLOSE было получено.   -  person Dave    schedule 24.12.2010
comment
@Dave Пожалуйста, попробуйте пример приложения, которое я предоставил - это уменьшит, завис ли ваш основной поток или он просто простаивает в ожидании сообщений.   -  person Tim Lloyd    schedule 24.12.2010


Ответы (5)


Идентификатор рабочего потока, который вы видите, равен 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);
    }
} 
person Tim Lloyd    schedule 21.12.2010
comment
@chibacity: я понимаю, что вы говорите - управляемый идентификатор равен 0, и это то, что возвращает Thread.CurrentThread.GetHashCode(), не так ли? Хм... Я рассмотрю то, что вы предложили, спасибо. - person Dave; 21.12.2010
comment
@Dave Идентификатор управляемого потока — это то, что управляется платформой и может быть получено через Thread.CurrentThread.ManagedThreadId. Он отличается от собственного идентификатора, которым управляет ОС. Поток с управляемым идентификатором 0 является потоком фреймворка, а не потоком, который вы создали или используете напрямую. Это первый поток, которым управляет фреймворк, поэтому его идентификатор равен 0. Он предшествует основному потоку с идентификатором 1 и потоку финализатора с идентификатором 2. - person Tim Lloyd; 21.12.2010
comment
@chibacity Имеет ли смысл, что идентификатор управляемого потока 0 приходит и уходит? Просто для проверки я приостановил выполнение приложения и проверил запущенные потоки. Я открывал разные диалоги, ставил на паузу, проверял темы, ставил на паузу. Иногда идентификатор управляемого потока 0 присутствует, иногда он исчезает, а иногда возвращается. Это что-нибудь значит? Я пытаюсь найти ресурсы в Интернете, которые объяснят значение потока 0. - person Dave; 23.12.2010
comment
@Dave Я не знаю кровавых подробностей о том, как ведут себя потоки фреймворка. Информации об этом в сети также мало - она ​​очень внутренняя, поэтому широко не опубликована\документирована. Я все еще думаю, что у вас есть проблема с вашим основным потоком, и вы лаете не в то дерево, глядя на поток 0. Я не могу помочь без исходного кода! Не могли бы вы сделать небольшой воспроизводимый образец? Если вы не можете, это еще более убедительное доказательство того, что это ваш основной поток, т.е. конкретная программа. - person Tim Lloyd; 23.12.2010
comment
@chibacity спасибо - извините, я перепутал и хотел узнать, где было обновление вашего ответа. :) я такой глупый. Я посмотрю на это. - person Dave; 24.12.2010
comment
@chibacity Я получаю PInvokeStackImbalance -- Вызов функции PInvoke 'HungGUITest!HungGUITest.Program::PostMessage' разбалансировал стек. Вероятно, это связано с тем, что управляемая подпись PInvoke не соответствует неуправляемой целевой подписи. Убедитесь, что соглашение о вызовах и параметры подписи PInvoke соответствуют целевой неуправляемой подписи. - person Dave; 24.12.2010
comment
@Dave Ой, плохо, подпись для PostMessage была по памяти. Я добавил в подпись два недостающих (но неиспользуемых в данном случае) параметра. Я пропустил это, так как вы получаете несбалансированную ошибку только тогда, когда запускаете ее с подключенным отладчиком! Пожалуйста, используйте обновленный код. :) - person Tim Lloyd; 24.12.2010
comment
@chibacity нет проблем, я должен был изучить это подробнее, но у меня нет большого опыта работы с PInvoke. Я запустил ваше приложение, и оно опубликовало сообщение, но мое приложение все еще работает. - person Dave; 24.12.2010
comment
@Dave Попробуйте запустить приложение, но вызовите QuitApplication вместо CloseMainWindow. - person Tim Lloyd; 24.12.2010
comment
@Dave Убедитесь, что к вашему приложению не подключен отладчик и вы не приостановили его потоки. - person Tim Lloyd; 24.12.2010
comment
@chibacity хорошо, я запустил отладчик и открыл диспетчер задач, чтобы проверить процесс. Я запустил ваше приложение с помощью CloseMainWindow и QuitApplication, и оно все еще работает. - person Dave; 24.12.2010
comment
@Dave Основной поток определенно зависает, поскольку он не обработал ни одно сообщение. Попробуйте отправить любое сообщение в другое приложение, вы увидите его выход, например, блокнот. - person Tim Lloyd; 24.12.2010
comment
@chibacity большое спасибо за вашу помощь. Думаю, мне придется прибегнуть к отладке грубой силы. - person Dave; 24.12.2010
comment
@Dave, возможно, у вас есть вызов Invoke или BeginInvoke, который зависает в основном потоке. Один из способов добиться этого — написать прокси-сервер IDispatcher и использовать его для всех ваших вызовов диспетчера. Просто переадресуйте вызовы диспетчеру приложений. После того, как у вас есть прокси, вы можете обернуть свои действия и добавить до\после ведения журнала. Вы должны быть в состоянии видеть, где находится ваше зависание, так как будет запись в журнале до, но не после. Рассмотрите возможность использования счетчика и условной точки останова, чтобы прибить его, как только вы узнаете, что такое шаблон. - person Tim Lloyd; 24.12.2010
comment
@chibacity отличное предложение, я бы никогда не подумал об этом. Спасибо! Я начну работать над этим. - person Dave; 24.12.2010
comment
@chibacity В итоге я выбрал более простой маршрут и установил точки останова для всех вызовов BeginInvoke. Ни один из них не был вызван в режиме сбоя, который я пытаюсь отладить. - person Dave; 28.12.2010
comment
@Dave Дэйв, я бы сосредоточился на создании самого маленького приложения, которое могло бы тогда воспроизвести проблему. - person Tim Lloyd; 28.12.2010
comment
@chibacity Да - сейчас я собираюсь попробовать метод грубой силы - откатывать мой код до тех пор, пока проблема не исчезнет, ​​а затем выяснить, какие изменения я внес в проект, из-за которого он сломался. Если повезет, такая точка отката существует, но также возможно, что когда я конвертировал проект в VS2010, проблема уже была. - person Dave; 28.12.2010
comment
@chibacity - см. мой ответ здесь - я понял проблему. Думаю, я неправильно понял, как MEF создает экземпляры объектов. Я собираюсь наградить вас наградой, но придется подождать 20 часов. Большое спасибо за то, что помогли мне узнать кое-что новое о .NET в процессе. - person Dave; 28.12.2010
comment
@Dave Дэйв Рад видеть, что ты докопался до сути - рад, что помог. Приветствия за щедрость. :) - person Tim Lloyd; 29.12.2010
comment
@chibacity без проблем. Прикол в том, что у меня такая же проблема в ветке, и когда я ее пропатчил, там проблема не исчезла. Но так как он работает в багажнике, это главное на данный момент. :) - person Dave; 29.12.2010

Добавьте следующий обработчик в каждое окно, которое ваше приложение создает в отдельном потоке:

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

Если основной поток завис, вызовите win.Dispatcher.InvokeShutdown() в MainWindow.Closed — это автоматически закроет все остальные окна, созданные в основном потоке.

Без этого обработчика мое приложение со следующим кодом тоже зависало при выходе:

void Worker() {
    var win = new Window();
    // win.Closed += onWindowClose ?? ((o, e) => editor.Dispatcher.InvokeShutdown());
    editor.Show();
    System.Windows.Threading.Dispatcher.Run();
}
person Mykola Bogdiuk    schedule 28.12.2010
comment
спасибо за предложение, я добавлю это в качестве превентивной меры, но в моем приложении есть только главное окно. Но никогда не знаешь... Я дам тебе знать, как все пройдет. :) - person Dave; 28.12.2010
comment
+1, это исправило связанную с этим досадную проблему, которая у меня была. Сторонний компонент зависал в приложении при завершении работы (я думаю, он, должно быть, создавал какое-то скрытое окно), и вызов этого из события Closed главного окна исправил это. - person Dan Bryant; 04.10.2012

Я наконец понял эту проблему. У меня был элемент управления, который был Imported MEF, но на самом деле никогда не вызывался (пока). Я думаю, что MEF создал его экземпляр, хотя он нигде не упоминался (я исходил из того, что создание не произошло пока ресурс не был запрошен, но, видимо, я ошибался). Я исправил проблему с помощью создания экземпляров Lazy‹>, и теперь это работает. Вот этот меня реально подкинул, но спасибо всем за помощь. Я многому научился, пытаясь отладить эту проблему.

person Dave    schedule 28.12.2010

Если вы создаете рабочие потоки (а не потоки пула), установите их Имя на что-то описательное при создании.

person Mitch Wheat    schedule 21.12.2010
comment
Если вы действительно в затруднительном положении, вы можете переименовать потоки пула, используя отражение. Если вы отчаянно нуждаетесь в курсе... - person Tim Lloyd; 21.12.2010
comment
это хорошее предложение на будущее, но я буквально устанавливаю точки останова в каждом месте, где я создал поток (поэтому, если я пропустил его там, я бы пропустил и имя :)). Есть ли другое место, где я могу установить точку останова при создании любого потока, например, в настройках IDE? - person Dave; 21.12.2010
comment
@Dave, когда вы используете такие конструкции, как BeginInvoke, вы фактически не создаете поток. Рабочий элемент ставится в очередь в пул потоков, и существующий поток забирает его. - person Tim Lloyd; 21.12.2010
comment
хорошо спасибо. Я знал, что BeginInvoke использует ThreadPool, но я думал, что они все еще отображаются в окне потоков как WorkerThread. Я думаю, нет? Мне нужно взглянуть... Я знаю, что они где-то там, потому что я видел их, проходя один шаг по моему коду. - person Dave; 21.12.2010
comment
@Mitch спасибо за предложение. Хотя это не решает мою проблему, мне очень нравится смотреть в окно Threads и теперь видеть что-то более значимое, чем просто Worker Thread. :) Намного лучше! - person Dave; 21.12.2010
comment
@Dave Да, потоки пула потоков появятся в окне потоков. Я имел в виду ваш вопрос о том, есть ли другое место, где я могу установить точку останова при создании любого потока. Это не позволит использовать потоки пула потоков, поскольку вы на самом деле их не создаете - вот и все. - person Tim Lloyd; 21.12.2010
comment
@chibacity А, я понимаю, что вы сказали. Спасибо за разъяснения. В ближайшее время рассмотрю ваш другой комментарий. - person Dave; 21.12.2010

Добавьте ведение журнала ко всему вашему коду, чтобы регистрировать метод и идентификатор потока. Вы можете использовать замену регулярного выражения, чтобы поместить его в начало каждого отдельного метода. Затем запустите приложение и выключите его. Посмотрите, какие сообщения журнала имеют тот же идентификатор потока, что и рабочий поток, который не закрывается.

person Samuel Neff    schedule 21.12.2010
comment
Идентификатор рабочего потока равен 0 — это поток фреймворка, хотя я не уверен, какова его роль. Я считаю, что это на самом деле проблема с основным потоком. - person Tim Lloyd; 21.12.2010
comment
благодаря. Это лучший совет на данный момент, я думаю. Я уже зарегистрировал всю эту информацию с помощью log4net, и мне даже не пришло в голову проследить поведение потока в журналах, чтобы таким образом получить контекст! Ницца. :) - person Dave; 21.12.2010
comment
Интересно, в логах нигде нет 3256, а 6524 было. На данный момент выполнение приложения выглядит совершенно нормально и работает нормально, пока я не выключу его. Я попробую изменить конфигурацию log4net, чтобы регистрировать все точки входа, и посмотрим, получится ли что-нибудь. - person Dave; 21.12.2010
comment
Идентификатор управляемого потока равен 0, а не собственному идентификатору. Это первый поток, которым управляет фреймворк, поэтому его идентификатор равен 0. Он предшествует основному потоку с идентификатором 1 и перед потоком финализатора с идентификатором 2. Идентификатор управляемого потока управляется framework и отличается от собственного идентификатора, на который вы ссылаетесь и которым управляет ОС. Поскольку это выделенный поток фреймворка, он не был порожден программой как таковой. Он был запущен до программы (т.е. до Main) и используется фреймворком. Все программы .Net имеют этот поток. - person Tim Lloyd; 21.12.2010