【发布时间】: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