【问题标题】:Is it possible for a thread that is not the UI thread to manipulate the UI elements?不是 UI 线程的线程是否可以操作 UI 元素?
【发布时间】:2018-12-08 03:05:20
【问题描述】:

我已经读到应该只允许 UI 线程操作 WinAPI 中的 UI 元素。但我认为不是 UI 线程的线程甚至不可能操作 UI 元素。

我认为因为当一个线程(不是UI线程)调用SendMessage()函数来操作一些UI元素时,会向UI线程发送一条消息,然后由UI线程来操作UI 元素,而不是其他线程。

我说的对吗?

【问题讨论】:

  • 很确定除了调用SendMessage之外,还有其他方法可以操作UI元素。
  • 只有创建HWND 的线程才能接收和发送该HWND 的消息,所以在HWND 上执行的所有实际工作都由拥有线程。但是其他线程当然可以发送消息到HWND,它们将被拥有的线程分派和处理。这在SendMessage() documentation 中有明确说明:...
  • ... "如果指定的窗口是由不同的线程创建的,则系统切换到该线程并调用相应的窗口过程。线程之间发送的消息只有在接收线程时才被处理执行消息检索代码。发送线程被阻塞,直到接收线程处理完消息。"
  • @user3386109 “很确定,除了调用 SendMessage 之外,还有其他方法可以操作 UI 元素。”您能举例说明这些其他方法吗?
  • @PaulMorris 从纯粹的 Win32 API 角度来看,跨线程操作 UI 控件不会出错,因为 API 会为您处理序列化(不计算多消息操作期间的竞争条件) )。此类警告通常更多地与 包装器库 相关联,这些包装器库分配与 UI 相关的额外资源,这些资源不是以线程安全方式管理的,因此跨线程操作更加危险。

标签: c multithreading winapi


【解决方案1】:

首先,假设是为了满足 OP 的好奇心:

  • 如果我们将操作 UI 元素定义为读取或写入 元素的属性,那么从技术上讲,你可以想出你的 自己的 UI 框架,可以独立维护元素 Windows API。这样的尝试have been made。 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() 中调用任何控制生成函数;这些include but are not limited toDialogBox()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 API 和preserve and maintain the proper lock hierarchy 时,您还必须非常小心。相当多的 User32 函数和许多阻塞 COM 调用属于这一类。

使用InSendMessage()ReplyMessage()(使用SendMessage() 时)或PostMessage() 及其同级可以缓解其中一些问题。但是,您会遇到各种控制流问题,因为您可能想知道在继续当前线程或处理下一条消息之前已处理该消息。因此,无论如何您最终都必须实现某种同步机制,但这变得越来越困难,需要避免许多陷阱。

问题也不仅限于在线程之间发送消息。从不同的线程更改WndProc() 会导致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!
}

此外,不正确地使用来自多个线程的 DC 可以lead to subtle bugs

这些原因以及其他原因(包括性能)可能导致标准 API 包装器(如 MFC 和 WinForms)的设计者简单地假设他们的 API 将在单线程上下文中使用。它们不提供任何线程安全保护,并且由用户来实现此类机制,但是更高级别的抽象使其 even easierneglect the underlying issues。当出现此类问题时,通常的答案是:不要使用来自所有者线程之外的控件。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-29
    • 1970-01-01
    • 2019-09-14
    • 2012-10-03
    • 2020-09-12
    相关资源
    最近更新 更多