【问题标题】:Deadlock in multi-threaded Windows GUI application多线程 Windows GUI 应用程序中的死锁
【发布时间】:2019-10-12 20:35:20
【问题描述】:

我为 Windows 10 开发了一个 DAW 应用程序。这是一个用 C++ 编写并由 Visual Studio 2019 构建的 x64 应用程序。

该应用程序使用一个不使用任何 Windows API 的自定义 GUI,但它还必须加载 VST 2.4 plugins确实使用标准 Win32 GUI,我在无模式弹出窗口(非子级)中打开它们窗户。

我一直试图解决的问题是死锁——见下文。

免责声明:我知道代码并不完美和优化 - 请这是一项正在进行的工作。

======== main.cpp =============================

// ...

void winProcMsgRelay ()
{
    MSG     msg;

    CLEAR_STRUCT (msg);

    while (PeekMessage(&msg, NULL,  0, 0, PM_REMOVE)) 
    { 
        TranslateMessage (&msg);
        DispatchMessage (&msg);
    };
}

// ...

int CALLBACK WinMain (HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdL, int nCmdShw)  
{
// ...
}

=================================================

1) WinMain 函数创建一个新线程来处理我们的自定义 GUI(不使用任何 Windows API)。

2) WinMain 线程使用标准的 Windows GUI API,它处理传递到我们的主应用程序窗口的所有窗口消息。

WinMain 线程通过调用CreateWindowEx(带有WNDPROC 窗口过程回调)来创建我们的主窗口:

{
    WNDCLASSEX  wc;

    window_menu = CreateMenu ();
    if (!window_menu)
    {
        // Handle error
        // ...
    }

    wc.cbSize = sizeof (wc);
    wc.style = CS_BYTEALIGNCLIENT | CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = mainWndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInst;
    wc.hIcon = LoadIcon (NULL, IDI_APP);
    wc.hCursor = NULL;
    wc.hbrBackground = NULL;
    wc.lpszMenuName = mainWinName;
    wc.lpszClassName = mainWinName;
    wc.hIconSm = LoadIcon (NULL, IDI_APP);
    RegisterClassEx (&wc);

    mainHwnd = CreateWindowEx (WS_EX_APPWINDOW | WS_EX_OVERLAPPEDWINDOW | WS_EX_CONTEXTHELP,
                                       mainWinName, mainWinTitle,
                                       WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                                       CW_USEDEFAULT, 0,
                                       0, 0,
                                       NULL, NULL, hInst, NULL);


    // ...

    // Then the WinMain thread keeps executing a standard window message processing loop 

    // ...
    while (PeekMessage (&msg, NULL, 0, 0, PM_NOREMOVE) != 0
           && ! requestQuit)
    {
        if (GetMessage (&msg, NULL, 0, 0) == 0)
        {
            requestQuit = true;
        }
        else
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        if (! requestQuit)
        {
            WaitMessage ();
        }
    }
    // ...
}

3) 我们的 custom-GUI 线程(在上面生成)除了它的其他功能外,还执行以下操作:

a) 通过调用 LoadLibrary 从 DLL 文件加载 VST 音频插件。

b) 为 DLL 插件创建一个新线程(我们称之为“plugin thread”)来创建它的一个新实例(加载的 DLL 插件可能有多个实例):

vst_instance_thread_handle = (HANDLE) _beginthreadex (NULL, _stack_size, redirect, (void *) this, 0, NULL);

c) 在插件实例在其自己的线程上运行一段时间后,我们的 自定义 GUI 线程(响应我们自定义 GUI 中的用户操作)为插件 GUI 窗口:

vst_gui_thread_handle = (HANDLE) _beginthreadex (NULL, _stack_size, redirect, (void *) this, 0, NULL);

(请注意,DLL 插件使用标准的 Win32 GUI。)

当新的插件GUI线程被生成时,函数VSTGUI_open_vst_gui插件实例线程上被调用——见下文:

    ============ vst_gui.cpp: ====================

// ...

struct VSTGUI_DLGTEMPLATE: DLGTEMPLATE
{
    WORD e[3];
    VSTGUI_DLGTEMPLATE ()
    {
        memset (this, 0, sizeof (*this));
    };
};

static INT_PTR CALLBACK VSTGUI_editor_proc_callback (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);

thread_local AEffect * volatile Vst_instance_ptr = 0;
thread_local volatile int Vst_instance_index = -1;
thread_local volatile UINT_PTR Vst_timer_id_ptr = 0;
thread_local volatile HWND Vst_gui_handle = NULL;

void VSTGUI_open_vst_gui (int vst_instance_index)
{
    AEffect *vst_instance = VST_instances [vst_instance_index].vst->pEffect;

    Vst_instance_index = vst_instance_index;
    Vst_instance_ptr = vst_instance;

    VSTGUI_DLGTEMPLATE t;   

    t.style = WS_POPUPWINDOW | WS_MINIMIZEBOX | WS_DLGFRAME | WS_VISIBLE |
                          DS_MODALFRAME | DS_CENTER;

    t.cx = 100; // We will set an appropriate size later
    t.cy = 100;


    VST_instances [vst_instance_index].vst_gui_open_flag = false;

    Vst_gui_handle = CreateDialogIndirectParam (GetModuleHandle (0), &t, 0, (DLGPROC) VSTGUI_editor_proc_callback, (LPARAM) vst_instance);

    if (Vst_gui_handle == NULL)
    {
        // Handle error
        // ...
    }
    else
    {
        // Wait for the window to actually open and initialize -- that will set the vst_gui_open_flag to true
        while (!VST_instances [vst_instance_index].vst_gui_open_flag)
        {
            winProcMsgRelay ();
            Sleep (1);
        }

        // Loop here processing window messages (if any), because otherwise (1) VST GUI window would freeze and (2) the GUI thread would immediately terminate.
        while (VST_instances [vst_instance_index].vst_gui_open_flag)
        {
            winProcMsgRelay ();
            Sleep (1);
        }
    }

    // The VST GUI thread is about to terminate here -- let's clean up after ourselves
    // ...
    return;
}



// The plugin GUI window messages are handled by this function:

INT_PTR CALLBACK VSTGUI_editor_proc_callback (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    AEffect* vst_instance = Vst_instance_ptr;
    int instance_index = Vst_instance_index;

    if (VST_instances [instance_index].vst_gui_window_handle == (HWND) INVALID_HANDLE_VALUE)
    {
        VST_instances [instance_index].vst_gui_window_handle = hwnd;
    }

    switch(msg)
    {
    case WM_INITDIALOG:
        {
            SetWindowText (hwnd, String (tmp_str) + VST_get_best_vst_name (instance_index, false));

            if (vst_instance)
            {
                ERect* eRect = 0;
                vst_instance->dispatcher (vst_instance, effEditGetRect, 0, 0, &eRect, 0);

                if (eRect)
                {
                    // ...

                    SetWindowPos (hwnd, HWND_TOP, x, y, width, height, SWP_SHOWWINDOW);
                }

                vst_instance->dispatcher (vst_instance, effEditOpen, 0, 0, hwnd, 0);
            }
        }   
        VST_instances [instance_index].vst_gui_open_flag = true;

        if (SetTimer (hwnd, (UINT_PTR) Vst_instance_ptr, 1, 0) == 0)
        {
            logf ("Error: Could not obtain a timer object for external VST GUI editor window.\n");  
        }

        return 1; 

    case    WM_PAINT:
        {
            PAINTSTRUCT ps;
            BeginPaint (hwnd, &ps);
            EndPaint (hwnd, &ps);
        }
        return 0;

    case WM_MOVE:

        if (Vst_instance_index >= 0)
        {
            VST_instances [Vst_instance_index].vst_gui_win_pos_x = VST_get_vst_gui_win_pos_x (Vst_instance_index);
            VST_instances [Vst_instance_index].vst_gui_win_pos_y = VST_get_vst_gui_win_pos_y (Vst_instance_index);
        }

        return 0; 

    case WM_SIZE:

        if (Vst_instance_index >= 0)
        {
            VST_instances [Vst_instance_index].vst_gui_win_width = VST_get_vst_gui_win_width (Vst_instance_index);
            VST_instances [Vst_instance_index].vst_gui_win_height = VST_get_vst_gui_win_height (Vst_instance_index);
        }

        return 0; 

    case WM_TIMER:

        if (vst_instance != NULL)
        {
            vst_instance->dispatcher (vst_instance, effEditIdle, 0, 0, 0, 0);
        }
        return 0;

    case WM_CLOSE:

        // ...

        return 0; 

    case WM_NCCALCSIZE:
        return 0;

    default:
        return (DefWindowProc (hwnd, msg, wParam, lParam));
    }

        return 0;
=================================================

我们的 custom-GUI 线程 也会定期循环调用 winProcMsgRelay (); Sleep (1);

为什么是多线程?因为:1)这是一个实时音频处理应用程序,需要接近零延迟,2)我们需要根据每个线程的实际需求独立设置 CPU 优先级和堆栈大小。此外,3) 拥有多线程 GUI 允许我们的 DAW 应用在插件或其 GUI 变得无响应时保持响应,并且 4) 我们使用多核 CPU。

一切正常。我可以打开多个插件的多个实例。他们的 GUI 窗口甚至可以生成其他窗口显示进度条,所有这些没有任何死锁

但是,问题是当我在插件 GUI 窗口(Native Instruments 的 Absynth 5 和 Kontakt 6)中单击应用程序徽标时出现死锁,这显然 创建了一个子模式窗口,顺便说一句,它正确且完整地显示。 但是这个模态窗口和父 GUI 窗口都停止响应用户操作和窗口消息——它们“挂起”(不过,我们的自定义 GUI 仍然运行良好)。当插件 GUI 在错误时显示标准 Windows 模式 MessageBox 时会发生同样的事情,其中​​ MessageBox 完全“冻结”。

当我在调用winProcMsgRelay的第二个循环中的VSTGUI_open_vst_gui中设置调试器断点时,我可以确定这是它挂起的地方,因为当我进入死锁状态时, 断点永远不会触发

我知道模态对话框有自己的消息循环,可能会阻塞我们的消息循环,但我应该如何重新设计我的代码以适应这种情况?

我也知道SendMessage 之类的会阻塞,直到他们得到响应。这就是我改用异步PostMessage 的原因。

我确认死锁也发生在应用程序的 32 位版本中。

几周以来,我一直在努力寻找原因。我相信我已经完成了所有的功课,老实说,我不知道还有什么可以尝试的。任何帮助将不胜感激。

【问题讨论】:

  • 为什么要制作多线程 UI?
  • 您的“标准消息循环”非常不标准。你为什么打电话给PeekMessage,然后是GetMessage?如果在您调用 PeekMessage 时队列中没有消息,它将返回 false 并且您的消息循环将在此时退出。
  • @JonathanPotter 不,只有当我们根据用户操作将 requestQuit 设置为 true 或收到标准退出事件时,循环才会退出。它已经过测试并且有效,相信我。为什么是 PeekMessage?来自 MS 文档:这两个函数之间的主要区别在于 GetMessage 直到将符合过滤条件的消息放入队列后才会返回,而 PeekMessage 会立即返回,无论消息是否在队列中。
  • @DavidHeffernan 不确定您的意思,但该应用程序使用许多独立的 GUI,一些自定义(主机),一些基于 Windows(主窗口应用程序和 dplugins)。
  • 并且自定义 GUI 用于保持 DAW 应用程序可移植到非 Windows 操作系统。此外,WinMain 线程中的 PeekMessage-GetMessage 循环没有挂起——我在调试器中验证了这一点,因为我写道。那不是罪魁祸首。最后,请阅读我文章开头的免责声明。

标签: c++ multithreading winapi dll vst


【解决方案1】:

这里有很多代码没有出现(例如 winProcMsgRelay),我承认我很难从脑海中了解它是如何工作的,但是让我为您提供一些一般性建议和一些需要注意的事项介意。

首先,模态对话框有自己的消息循环。只要它们启动,您的消息循环就不会运行。

其次,像 SetWindowPos SetWindowText 这样的窗口函数实际上是向窗口发送消息。您是从创建窗口的线程中调用那些吗?因为如果不是,这意味着调用线程将在操作系统将消息发送到窗口并等待响应时阻塞。如果创建这些窗口的线程正忙,则发送线程将保持阻塞状态,直到它不忙为止。

如果我试图调试它,我会等到它死锁,然后闯入调试器并调出线程并调用堆栈窗口彼此相邻。在线程窗口中的线程之间切换上下文(双击它们)并查看生成的线程调用堆栈。您应该能够发现问题。

【讨论】:

  • winProcMsgRelay 的完整代码实际上在我帖子的顶部。如果需要,我还可以发布更多代码。就告诉我嘛。我知道模态对话框有自己的消息循环,但我应该如何重新设计我的代码以适应它?我也知道 SendMessage 之类的东西在得到响应之前一直处于阻塞状态。这就是我改用异步 PostMessage 的原因。最后,调试器暂停了我已经做过的建议,正如我所写的,所以我知道死锁在哪里。谢谢。
  • 您似乎已经解决了,但如果您再次遇到此问题,我想回复一下。抱歉,我错过了 winProcMsgRelay 但在您的帖子中缺少第一行(标题!)所以我看不到。此外,虽然使用 PostMessage 很好,但您也在使用 SetWindowPos 和 SetWindowText 并且这些调用不会发布它们的消息。他们阻止。这些调用是从创建这些窗口的线程中发生的吗?最后,我认为重新设计的方法是将所有窗口保留在一个 UI 线程中,并且没有任何后台线程等待它们。多个 UI 线程不是要走的路
【解决方案2】:

好的,我自己解决了这个僵局。解决方案是重写代码以统一窗口 proc 处理程序(VST GUI 消息由与主窗口消息相同的回调函数处理)。此外,与使用 DialogBoxIndirectParam 创建插件窗口的官方 VST SDK 不同,我现在使用 CreateWindowEx(虽然不确定这是否有助于解决死锁问题)。感谢 cmets。

【讨论】:

  • 很难看出这对未来的读者有多大用处,因为它对您自己的代码非常具体,我们看不到。如果我是你,我会删除整个帖子。
  • @DavidHeffernan 老实说,我没有看到任何可以使其特定于我的代码的东西。但最重要的是,有人可以以类似的方式设计他们的程序并遇到同样的问题。如果他们遵循我的解决方案,我不明白为什么它不应该帮助他们。
  • 如何知道他们的问题是否与您的问题相同?没有清晰的复制,只是描述可能缺少重要细节。
猜你喜欢
  • 2014-07-19
  • 1970-01-01
  • 1970-01-01
  • 2018-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多