多线程 Windows GUI 应用程序中的死锁

Deadlock in multi-threaded Windows GUI application

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

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

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

免责声明:我知道代码并不完美和优化——请继续努力。

======== 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 并且它处理传递给我们的主应用程序的所有 window 消息 window.

WinMain 线程 通过调用 CreateWindowEx(使用 WNDPROC window 过程回调创建我们的主 window:

{
    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) 我们的 自定义 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 创建一个新线程 window:

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;
=================================================

我们的 自定义 GUI 线程 也定期在循环中调用 winProcMsgRelay (); Sleep (1);

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

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

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

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

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

我也知道 SendMessage 之类的人在得到回应之前一直处于阻塞状态。这就是我使用异步 PostMessage 的原因。

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

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

这里有很多代码没有出现(例如 winProcMsgRelay),我承认我发现很难在脑海中了解它是如何工作的,但让我为您提供一些一般性建议和一些事情记住。

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

其次,windows 功能类似于 SetWindowPos SetWindowText 实际上 发送消息 到 window。您是从创建 window 的线程调用它们吗?因为如果没有,这意味着调用线程将在 OS 将消息发送到 window 并等待响应时阻塞。如果创建这些 windows 的线程正忙,则发送线程将保持阻塞状态,直到它不忙为止。

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

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