Почему ввод блокировки в потоке пользовательского интерфейса вызвал событие OnPaint? - PullRequest
9 голосов
/ 08 декабря 2011

Я наткнулся на то, чего просто не понимаю.В моем приложении есть несколько потоков, которые все добавляют (и удаляют) элементы в общую коллекцию (используя общую блокировку).Поток пользовательского интерфейса использует таймер, и на каждом тике он использует коллекцию для обновления своего пользовательского интерфейса.

Поскольку мы не хотим, чтобы поток пользовательского интерфейса долго удерживал блокировку и блокировал другие потоки.То, как мы это делаем, заключается в том, что сначала мы получаем блокировку, копируем коллекцию, снимаем блокировку и затем работаем над нашей копией.Код выглядит следующим образом:

public void GUIRefresh()
{
    ///...
    List<Item> tmpList;
    lock (Locker)
    {
         tmpList = SharedList.ToList();
    }
    // Update the datagrid using the tmp list.
}

Хотя он работает нормально, мы заметили, что иногда в приложении возникают замедления, и когда нам удалось поймать трассировку стека, мы увидели это:

....
at System.Windows.Forms.DataGrid.OnPaint(PaintEventArgs pe)
at MyDataGrid.OnPaint(PaintEventArgs pe)
at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)
at System.Windows.Forms.Control.WmPaint(Message& m)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Threading.Monitor.Enter(Object obj)
at MyApplication.GuiRefresh()   
at System.Windows.Forms.Timer.OnTick(EventArgs e)
at System.Windows.Forms.Timer.TimerNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
....

Обратите внимание, что за вводом блокировки (Monitor.Enter) следует NativeWindow.Callback, что приводит к OnPaint.

  • Как это возможно?Потеряли ли поток пользовательского интерфейса для проверки сообщения?Имеет ли это смысл?Или здесь есть что-то еще?

  • Есть ли способ избежать этого?Я не хочу, чтобы OnPaint вызывался из замка.

Спасибо.

Ответы [ 3 ]

16 голосов
/ 08 декабря 2011

Основной поток приложения с графическим интерфейсом - это поток STA, Single Threaded Apartment.Обратите внимание на атрибут [STAThread] в методе Main () вашей программы.STA - это термин COM, он предоставляет гостеприимный дом для компонентов, которые в основном не поддерживают потоки, что позволяет вызывать их из рабочего потока.COM все еще очень жив в приложениях .NET.Перетаскивание, буфер обмена, диалоги оболочки, такие как OpenFileDialog, и общие элементы управления, такие как WebBrowser, являются однопоточными COM-объектами.STA является жестким требованием для потоков пользовательского интерфейса.

Поведенческий контракт для потока STA заключается в том, что он должен прокачать цикл сообщений и не может блокироваться.Блокировка, скорее всего, приведет к взаимоблокировке, поскольку она не позволяет выполнить маршалинг для этих COM-компонентов с многопоточным соединением.Вы блокируете поток с помощью оператора lock .

CLR очень хорошо знает об этом требовании и что-то с ним делает.Блокирующие вызовы, такие как Monitor.Enter (), WaitHandle.WaitOne / Any () или Thread.Join () прокачивают цикл обработки сообщений.Тип собственного API-интерфейса Windows, который делает это, MsgWaitForMultipleObjects (). Этот цикл обработки сообщений отправляет сообщения Windows для поддержания работоспособности STA, включая сообщения рисования.Конечно, это может вызвать проблемы с повторным входом, Paint не должен быть проблемой.

В этом посте Chris Brumme есть хорошая справочная информация .

Может быть, этовсе звучит как звонок, вы, вероятно, не можете не заметить, что это звучит очень похоже на приложение, вызывающее Application.DoEvents ().Вероятно, самый страшный метод для решения проблем зависания пользовательского интерфейса.Это довольно точная ментальная модель того, что происходит внутри, DoEvents () также прокачивает цикл сообщений.Единственное отличие состоит в том, что эквивалент CLR немного более избирателен в отношении того, какие сообщения он разрешает отправлять, он фильтрует их.В отличие от DoEvents (), который отправляет все.К сожалению, ни пост Брамме, ни источник SSCLI20 не достаточно детализированы, чтобы точно знать, что отправляется, реальная функция CLR, которая делает это, недоступна в источнике и слишком велика для декомпиляции.Но ясно видно, что он не фильтрует WM_PAINT.Он будет отфильтровывать настоящих создателей проблемы, вводить уведомления о событиях наподобие того, который позволяет пользователю закрыть окно или нажать кнопку.

Функция, а не ошибка.Избегайте головных болей повторного входа, сняв блокировку и полагаясь на обратные вызовы с маршалингом.BackgroundWorker.RunWorkerCompleted является классическим примером.

4 голосов
/ 08 декабря 2011

Хороший вопрос!

Все ожидания в .NET являются «оповещаемыми». Это означает, что если блок ожидания блокируется, Windows может выполнить «асинхронные вызовы процедур» поверх стека ожидания. Это может включать обработку некоторых сообщений Windows. Я специально не пробовал WM_PAINT, но, судя по вашим наблюдениям, он включен.

Некоторые ссылки MSDN:

Функции ожидания

Асинхронные вызовы процедур

Книга Джо Даффи «Параллельное программирование в Windows» также охватывает это.

0 голосов
/ 04 мая 2014

Я нашел этот вопрос, когда возникла проблема с блокировкой на ручке ожидания.Ответы на это дали мне подсказку для реализации следующего:

 public static class NativeMethods
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern UInt32 WaitForSingleObject(SafeWaitHandle hHandle, UInt32 dwMilliseconds);
}

public static class WaitHandleExtensions
{
    const UInt32 INFINITE = 0xFFFFFFFF;
    const UInt32 WAIT_ABANDONED = 0x00000080;
    const UInt32 WAIT_OBJECT_0 = 0x00000000;
    const UInt32 WAIT_TIMEOUT = 0x00000102;
    const UInt32 WAIT_FAILED = INFINITE;

    /// <summary>
    /// Waits preventing an I/O completion routine or an APC for execution by the waiting thread (unlike default `alertable`  .NET wait). E.g. prevents STA message pump in background. 
    /// </summary>
    /// <returns></returns>
    /// <seealso cref="/5424534/pochemu-vvod-blokirovki-v-potoke-polzovatelskogo-interfeisa-vyzval-sobytie-onpaint">
    /// Why did entering a lock on a UI thread trigger an OnPaint event?
    /// </seealso>
    public static bool WaitOneNonAlertable(this WaitHandle current, int millisecondsTimeout)
    {
        if (millisecondsTimeout < -1)
            throw new ArgumentOutOfRangeException("millisecondsTimeout", millisecondsTimeout, "Bad wait timeout");
        uint ret = NativeMethods.WaitForSingleObject(current.SafeWaitHandle, (UInt32)millisecondsTimeout);
        switch (ret)
        {
            case WAIT_OBJECT_0:
                return true;
            case WAIT_TIMEOUT:
                return false;
            case WAIT_ABANDONED:
                throw new AbandonedMutexException();
            case WAIT_FAILED:
                throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
            default:
                return false;
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...