Sunday, July 8, 2018

Is it possible for a thread that is not the UI thread to manipulate the UI elements?

Leave a Comment

I have read that only the UI thread should be allowed to manipulate the UI elements in WinAPI. But I don't think that it is even possible for a thread that is not the UI thread to manipulate the UI elements.

I think that because when a thread (that is not the UI thread) calls the SendMessage() function to manipulate some UI element, a message will be sent to the UI thread, and then it is the UI thread that will manipulate the UI element and not the other thread.

Am I correct?

1 Answers

Answers 1

First, hypothetically speaking in an attempt to satisfy the OP's curiosity:

  • If we define manipulating UI elements as reading from or writing to elements' properties, then technically you could come up with your own UI framework that would maintain the elements independently from the Windows API. Such attempts have been made. WPF is one of them. You could then theoretically make the framework thread-safe and make it possible to access the elements' properties from multiple threads.
  • Also, GDI allows access to its objects from multiple threads, so you could potentially draw to your window from multiple threads (ditto for DirectX). WPF for example has a dedicated render thread (or at least it used to). You could also specify a different thread to process input with AttachThreadInput.

However, given the premise of the question that we're sticking to using the standard Windows API for creating and managing the UI, it is safe to say that access to the window is only achieved from within the thread that created it, because SendMessage() will switch to the owner thread. But that's not to say that invoking SendMessage() from multiple threads is a safe or a recommended approach. On the contrary, it is fraught with peril and care would have to be taken to properly synchronize the threads.

For one thing, a typical WndProc() looks like this:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {     ...     switch (message)     {         case WM_MYMSG1:             ...             SendMessage(hWnd, WM_MYMSG2, wParam, lParam);             ...         break;         ...         }     ... } 

So in order to protect your WndProc() so it can be accessed from multiple threads, you would have to make sure to use a reentrant lock, and not a semaphore, for example.

Secondly, if you use a reentrant lock you must make sure that it is only used within WndProc() or even make it specific to a message. Otherwise it is very easy to get into a deadlock:

//Worker thread: void foo ()  {     EnterCriticalSection(&g_cs);     SendMessage(hWnd, WM_MYMSG1, NULL, NULL);     LeaveCriticalSection(&g_cs);  }   //Owner thread: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {     switch (message)     {         case WM_MYMSG1:         {             EnterCriticalSection(&g_cs); //Deadlock!             ...             LeaveCriticalSection(&g_cs);          }         break;     } } 

Thirdly, you would have to make sure not to invoke any control-yielding functions within your WndProc(); these include but are not limited to: DialogBox(), MessageBox() and GetMessage(). Else you get a deadlock.

Then, consider a multi-window application, with each window's message pump being run in a separate thread. You would have to ensure not to send any messages between the threads in order not to end up with a deadlock:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {     ...     switch (message)     {         case WM_MYMSG1:             ...             SendMessage(hWnd2, WM_MYMSG1, wParam, lParam); //Deadlock!             ...         break;         ...         }     ... } 

You would also have to be very careful with using Windows APIs that implicitly manage the operating system's process-specific locks, and preserve and maintain the proper lock hierarchy. Quite a few User32 functions and many blocking COM calls fall into this category.

Some of these issues may be alleviated by using InSendMessage() and ReplyMessage() (when using SendMessage()) or PostMessage() and its siblings. However then you get into all kinds of control flow issues, because you may want to know that the message was processed before continuing the current thread or processing the next message. So you end up having to implement some kind of a synchronization mechanism anyway, but this becomes increasingly difficult with many pitfalls to avoid.

Problems don't stop with just sending messages between threads either. Changing WndProc() from a different thread can lead to terrible race-condition bugs:

//in UI thread: wpOld = (WNDPROC)GetWindowLongPtr(hwnd, GWLP_WNDPROC); //in another thread: SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)otherWndProc); //back in UI thread: SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)newWndProc); //still in UI thread: LRESULT CALLBACK newWndProc(...) {     CallWindowProc(wpOld, ...); //Wrong wpOld! } 

Also, improperly using DCs from multiple threads can lead to subtle bugs.

These reasons, and others (including performance), may have led the designers of standard API wrappers like MFC and WinForms to simply assume that their APIs will be used in a single-thread context. They don't offer any thread-safety protections and it's up to the user to implement such mechanisms, however the higher level of abstraction makes it even easier to neglect the underlying issues. When such problems arise, usually the answer is: don't use the control from outside the owner thread.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment