Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

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 для создания окон.

Из этого можно сделать следующие выводы:

  1. WinForms не изобретает колесо - лишь использует доступную информация для выполнения нужных проверок
  2. Если окно не было создано (нет дексриптора), а также не были созданы все его родители - 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 коммент.:

Отправить комментарий

Copyright 2007-2011 Chabster