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

Deadlock in multi-threaded Windows GUI application

本文关键字:死锁 应用程序 Windows GUI 多线程      更新时间:2023-10-16

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

该应用程序使用一个不使用任何Windows API的自定义GUI,但它还必须加载VST 2.4插件,确实使用标准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) 我们的自定义GUI线程(上面派生的),除了它的其他功能外,还执行以下操作:

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

b) 为DLL插件创建一个新线程(我们称之为">插件线程"),以创建它的新实例(加载的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;
=================================================

我们的自定义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位构建中。

几个星期以来,我一直在努力寻找原因。我相信我已经完成了所有的家庭作业,老实说,我不知道还能做什么。如有任何帮助,我们将不胜感激。

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

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

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

如果我试图调试它,我只需等待它死锁,然后进入调试器,打开相邻的线程和调用堆栈窗口。在线程窗口中的线程之间切换上下文(双击它们),并查看生成的线程调用堆栈。你应该能够发现问题。

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