Тупик в многопоточном приложении Windows GUI - PullRequest
0 голосов
/ 12 октября 2019

Я разрабатываю приложение DAW для Windows 10. Это приложение x64, написанное на C ++ и созданное Visual Studio 2019.

Приложение использует пользовательский графический интерфейс, не использующий WindowsAPI, но он также должен загружать VST 2.4 плагины , которые do используют стандартный графический интерфейс Win32, и я открываю их в немодальных всплывающих (не дочерних) окнах.

ПроблемаЯ пытался найти выход из тупика - см. Ниже.

Отказ от ответственности: я знаю, что код не идеален и не оптимизирован - пожалуйста, работа в процессе.

======== 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 создает новый поток, который будет обрабатывать наш пользовательский графический интерфейс (который не использует API Windows).

2) Поток WinMain использует стандартный Windows GUI API и обрабатывает все оконные сообщения, доставленные в наше главное окно приложения.

Поток WinMain создает наше главное окно, вызывая CreateWindowExWNDPROC обратный вызов оконной процедуры):

{
    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) Наш поток пользовательского интерфейса (порожденный выше), в дополнение к другим функциям, выполняет следующие действия:

a) Загружает аудио-плагин VST из файла DLL, вызывая LoadLibrary.

b) Создает новый поток для плагина DLL (назовем его « поток плагина ») длясоздайте его новый экземпляр (может быть несколько экземпляров загруженного подключаемого модуля DLL):

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

c) Через некоторое время, когда экземпляр подключаемого модуля запущен в своем собственном потоке, наш пользовательский -GUI-поток (в ответ на действие пользователя в нашем пользовательском графическом интерфейсе) создаетновый поток для окна GUI плагина:

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

(Обратите внимание, что плагин DLL использует стандартный графический интерфейс Win32.)

Когда новый поток 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;
=================================================

Наш поток пользовательского интерфейса тоже периодически вызываетwinProcMsgRelay (); Sleep (1); в цикле.

Почему многопоточный? Потому что: 1) это приложение для обработки звука в реальном времени, где требуются почти нулевые задержки, и 2) нам нужно устанавливать приоритеты ЦП и размеры стеков независимо для каждого потока, исходя из их реальных потребностей. Кроме того, 3) наличие многопоточного графического интерфейса позволяет нашему DAW-приложению оставаться отзывчивым, когда плагин или его графический интерфейс перестает отвечать на запросы, и 4) мы используем многоядерные процессоры.

Все работает хорошо. Я могу открыть несколько экземпляров нескольких плагинов. Их окна с графическим интерфейсом могут даже вызывать другие окна , показывающие индикаторы выполнения, все это без любого тупика .

Однако проблема в том, что я захожу в тупик, когда нажимаю логотип приложения в окне графического интерфейса плагина (Absynth 5 и Kontakt 6 от Native Instruments), которое, по-видимому, создает дочернее модальное окно , который, кстати, отображается правильно и полностью. Но и это модальное окно, и родительское окно GUI перестают отвечать на действия пользователя и оконные сообщения - они «зависают» (хотя наш настраиваемый графический интерфейс продолжает работать хорошо). То же самое происходит, когда плагин GUI отображает стандартный модал Windows MessageBox при ошибке, где MessageBox полностью «заморожен».

Когда я устанавливаю точку останова отладчика в VSTGUI_open_vst_gui во втором цикле, который вызывает winProcMsgRelay, , я могу определить, что это место, где он зависает , потому что, когда я получаю тупикутверждают, что точка останова никогда не срабатывает .

Я знаю, что модальные диалоги имеют свой собственный цикл сообщений, который может блокировать наш, но как мне изменить дизайн своего кода, чтобы приспособиться к этому?

Я также знаю, что SendMessage и т.п. блокируются, пока не получат ответ. Поэтому вместо этого я использую асинхронный PostMessage.

Я подтвердил, что взаимная блокировка также возникает в 32-разрядных сборках приложения.

Я пытался отследить причину в течение нескольких недель. Я полагаю, что я сделал всю мою домашнюю работу, и я честно не знаю, что еще попробовать. Любая помощь будет принята с благодарностью.

Ответы [ 2 ]

0 голосов
/ 13 октября 2019

Хорошо, я смог решить тупик самостоятельно. Решением было переписать код, чтобы унифицировать обработчики оконных процедур (сообщения VST GUI обрабатываются той же функцией обратного вызова, что и сообщения главного окна). Более того, в отличие от официального VST SDK, который использует DialogBoxIndirectParam для создания окна плагина, я теперь вместо этого использую CreateWindowEx (хотя не уверен, что это способствовало решению проблемы взаимоблокировки). Спасибо за комментарии.

0 голосов
/ 13 октября 2019

Здесь не так много кода (например, winProcMsgRelay), и я признаю, что мне сложно понять, как это работает, но позвольте мне дать вам несколько общих советов и некоторые вещи, которые следует придерживатьсяmind.

Прежде всего, модальные диалоги имеют свой собственный цикл сообщений. Пока они работают, ваш цикл сообщений не будет работать.

Во-вторых, функции Windows, такие как SetWindowPos, SetWindowText на самом деле отправляют в окно сообщение . Вы вызываете тех из потока, который создал окно? Потому что, если нет, это означает, что вызывающий поток будет блокироваться, в то время как ОС отправляет сообщение в окно и ожидает ответа. Если поток, создавший эти окна, занят, отправляющий поток останется заблокированным до тех пор, пока он не будет.

Если бы я пытался отладить это, я просто подождал, пока он заблокируется, затем прорвался в отладчик и вывел бывверх темы и стеки вызовов окна рядом друг с другом. Переключите контекст между потоками в окнах потоков (дважды щелкните по ним) и посмотрите, как получаются стеки вызовов потоков. Вы должны быть в состоянии определить проблему.

...