多线程 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(但不确定这是否有助于解决死锁问题)。感谢评论。
我为 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
.
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(但不确定这是否有助于解决死锁问题)。感谢评论。