非 UI 线程能否操作 UI 元素?

4

我读到过在WinAPI中只有UI线程才应该允许操作UI元素。但我认为,不是UI线程的线程根本无法操作UI元素。

我认为这是因为如果一个非UI线程的线程调用SendMessage()函数来操作某个UI元素,那么会向UI线程发送一条消息,然后是UI线程来操作UI元素而不是其他线程。

我的理解正确吗?


2
只有创建 HWND 的线程才能接收和分派该 HWND 的消息,因此所有在 HWND 上执行的实际工作都由拥有它的线程完成。但是其他线程肯定可以向 HWND 发送消息,并且它们将由拥有它的线程分派和处理。这在 SendMessage() 文档中已经明确说明:... - Remy Lebeau
1
如果指定的窗口是由不同的线程创建的,系统将切换到该线程并调用适当的窗口过程。在线程之间发送的消息仅在接收线程执行消息检索代码时处理。发送线程被阻塞,直到接收线程处理消息。 - Remy Lebeau
“我相信除了调用SendMessage之外,还有其他方法可以操作UI元素。” 您能举出这些其他方法的例子吗? - Paul Morris
1
从纯Win32 API的角度来看,跨线程操作UI控件时很少会出现问题,因为API会为您处理序列化(不包括多消息操作期间的竞争条件)。这样的警告通常与包装库有关,这些库分配了与UI相关的其他资源,并且以非线程安全的方式进行管理,因此在跨线程操作时更加危险。 - Remy Lebeau
@Remy Lebeau:“从纯Win32 API的角度来看,跨线程操作UI控件时不会出现太多问题。” 不确定,但我认为你误解了我的问题,我将澄清一下:当非UI线程想要操作UI控件时,它使用SendMessage()函数来实现,但是SendMessage()函数并不是用来操作UI控件的(即它不会在内存中操作表示UI控件的数据结构),而是向UI线程发送消息... - Paul Morris
显示剩余9条评论
1个回答

2
首先,为了满足OP的好奇心,让我们假设一下:
  • 如果我们将操作UI元素定义为从元素属性中读取或写入信息,那么你可以自己设计一个UI框架,独立于Windows API来维护这些元素。类似的尝试 已经存在,WPF就是其中之一。理论上,你可以使该框架线程安全,并使其可以从多个线程访问元素的属性。
  • GDI也允许从多个线程访问其对象,因此您可以从多个线程向窗口绘制内容(DirectX也是如此)。例如,WPF有一个专用的渲染线程(至少曾经有过)。您还可以使用AttachThreadInput指定不同的线程处理输入。
然而,鉴于问题的前提是我们坚持使用标准的Windows API来创建和管理UI,可以肯定的是,只有创建窗口的线程才能访问该窗口,因为SendMessage()会切换到所属线程。但这并不意味着从多个线程调用SendMessage()是安全或推荐的方法。相反,这样做充满了危险,必须小心地同步线程。
首先,典型的WndProc()看起来像这样:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    ...
    switch (message)
    {
        case WM_MYMSG1:
            ...
            SendMessage(hWnd, WM_MYMSG2, wParam, lParam);
            ...
        break;
        ...    
    }
    ...
}

因此,为了保护您的WndProc(),使其可以从多个线程访问,您必须确保使用可重入锁,而不是信号量。

其次,如果您使用可重入锁,必须确保仅在WndProc()内部使用它,甚至将其限定于特定消息。否则很容易陷入死锁:

//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;
    }
}

第三点,您必须确保在WndProc()内不调用任何控制器函数;其中包括但不限于:DialogBox()MessageBox()GetMessage()。否则会导致死锁。
然后,考虑一个多窗口应用程序,每个窗口的消息泵都在单独的线程中运行。您必须确保不在线程之间发送任何消息,以避免死锁的发生:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    ...
    switch (message)
    {
        case WM_MYMSG1:
            ...
            SendMessage(hWnd2, WM_MYMSG1, wParam, lParam); //Deadlock!
            ...
        break;
        ...    
    }
    ...
}

你还必须非常小心地使用Windows APIs,这些API隐含地管理操作系统的进程特定锁,并保留和维护正确的锁层次结构。很多User32函数和许多阻塞的COM调用都属于此类别。
通过使用InSendMessage()和ReplyMessage()(当使用SendMessage())或PostMessage()及其兄弟的一些问题可能会得到缓解。然而,这样做会涉及各种控制流问题,因为在继续当前线程或处理下一个消息之前,您可能需要知道消息是否被处理。因此,您最终不得不实现某种同步机制,但这会变得越来越困难,需要避免许多陷阱。
问题不仅仅停留在线程之间发送消息上。从不同的线程更改 WndProc() 可能会导致可怕的竞态条件错误。
//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!
}

此外,不正确地在多个线程中使用DC可能会导致微妙的错误
这些原因以及其他原因(包括性能),可能导致标准API包装器(如MFC和WinForms)的设计者假定他们的API将在单线程上下文中使用。它们不提供任何线程安全保护,用户需要实现这样的机制,但更高级别的抽象使得忽略底层问题 变得更容易。当出现这样的问题时,通常的答案是:不要从所有者线程之外使用控件。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接