Cruel InvokeRequired
Всем давно известно, что в WinForms начиная с версии 2 появилась защита от многопоточного использования элементов управления. При попытке выполнить опасные операции библиотечный код выбрасывает InvalidOperationException
с текстом Cross-thread operation not valid: Control 'xxx' accessed from a thread other than the thread it was created on
. Дальше я объясню как выполняется эта проверка и о некоторых подводных камнях этого механизма.
Свойство Handle
класса Control
имеет нетривиальную логику, часть которой содержит проверку на безопасность использования даного кода из другого потока:
[ Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), DispId(NativeMethods.ActiveX.DISPID_HWND), SRDescription(SR.ControlHandleDescr) ] public IntPtr Handle { get { if (checkForIllegalCrossThreadCalls && !inCrossThreadSafeCall && InvokeRequired) { throw new InvalidOperationException(SR.GetString(SR.IllegalCrossThreadCall, Name)); } if (!IsHandleCreated) { CreateHandle(); } return HandleInternal; } }
Основная часть условия - свойство InvokeRequired
:
[ Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden), SRDescription(SR.ControlInvokeRequiredDescr) ] public bool InvokeRequired { get { using (new MultithreadSafeCallScope()) { HandleRef hwnd; if (IsHandleCreated) { hwnd = new HandleRef(this, Handle); } else { Control marshalingControl = FindMarshalingControl(); if (!marshalingControl.IsHandleCreated) { return false; } hwnd = new HandleRef(marshalingControl, marshalingControl.Handle); } int pid; int hwndThread = SafeNativeMethods.GetWindowThreadProcessId(hwnd, out pid); int currentThread = SafeNativeMethods.GetCurrentThreadId(); return(hwndThread != currentThread); } } }
Что здесь происходит? В локальную переменную hwnd
записывается дескриптор текущего окна (если оно создано), иначе - дескриптор первого созданного окна в иерархии child-parent (метод FindMarshalingControl
). Если ни один родитель не создан (нет дескриптора), метод InvokeRequired
возвращает false
. Далее используются функции GetWindowThreadProcessId
и GetCurrentThreadId
чтобы определить принадлежность созданного окна текущему потому. ОС Windows запоминает идентификаторы потоков в контексте которых произошли вызовы CreateWindow
для создания окон.
Из этого можно сделать следующие выводы:
- WinForms не изобретает колесо - лишь использует доступную информация для выполнения нужных проверок
- Если окно не было создано (нет дексриптора), а также не были созданы все его родители -
InvokeRequired
возвращаетfalse
, что весьма логично - объект CLR может быть создан в любом потоке, но получит привязку к конкретному потоку лишь после создания
В результате следующий код содержит потенциальную проблему:
void handleNotificationFromOtherThread(...) { if (someControl.InvokeRequired) { someControl.BeginInvoke(handleNotificationFromOtherThread, ...); } // Thread safe code here ... }
Если уведомления прийдут до того, как хоть одно окно из иерархии будет создано, InvokeRequired
вернет false
и код выполнится в неправильном контексте. И здесь даже механизм защиты WinForms не поможет. В результате получаем многопоточный доступ к ресурсам без блокировок.
Как избежать подобного сценария? Создавая окно специально для целей синхронизации доступа:
Control sync = new Control(); sync.CreateControl(); _syncInvoke = (ISynchronizeInvoke)sync; ... void handleNotificationFromOtherThread(...) { if (_syncInvoke.InvokeRequired) { _syncInvoke.BeginInvoke(handleNotificationFromOtherThread, ...); } // Thread safe code here ... }
Код выше принудительно создает окно в нужном контексте и использует его интерфейс ISynchronizeInvoke
.
Но есть и более изящное решение - сохранить System.Threading.SynchronizationContext.Current
как член класса и использовать для синхронизации. WinForms сам создаст по одному окну специально для маршаллинга вызовов в каждом потоке. Этот подход лучше всего подходит для простых сценариев, когда нужно отмаршаллить все выховы в один главный поток.
0 коммент.:
Отправить комментарий