【问题标题】:Waiting on a handle in Windows Thread在 Windows 线程中等待句柄
【发布时间】:2018-06-08 11:45:11
【问题描述】:

我有一个 MFC 应用程序,它使用 CreateProcess(...) 启动另一个进程。我想在创建的进程终止时执行 UI 更新。通常,我会在返回的进程HANDLE 上使用WaitForSingleObjectWaitForMutlipleObject,但这会阻塞GUI 线程(不好)。

我能想到的唯一解决方案是生成一个可以在句柄上等待并在进程终止时发布消息的新线程。这并不理想。

那么是否可以向 Windows 管理器注册句柄并在进程终止时接收 Windows 消息?

【问题讨论】:

  • 为什么生成新线程不理想?
  • 线程很昂贵
  • @doron 请说明线程太贵的用例。你量过吗?它真的是应用程序中最消耗内存的部分吗?默认情况下,它的堆栈占用 1MB RAM。您也可以按比例缩小。但总的来说,这种说法是无稽之谈。 1个线程不贵,1000个可能。
  • 线程很昂贵:也许吧,但是使用CreateProcess 启动另一个进程的成本要高得多,因此创建线程不会对性能产生任何影响。
  • 使用RegisterWaitForSingleObject

标签: c++ winapi mfc


【解决方案1】:

当进程结束时,您可以使用RegisterWaitForSingleObject() 通过回调获得通知。 RegisterWaitForSingleObject 函数指示thread pool 中的等待线程等待进程,因此这应该是资源的最佳使用。正如 Raymond Chen 所说:

线程池可以将多个等待请求批处理到一个调用中 WaitForMultipleObjects 所以摊销成本是线程的 1/63。

下面是一个 Win32 GUI 应用程序的最小示例。代码创建一个窗口,然后创建另一个自身实例作为子进程,由“/child”参数指示。它注册等待回调函数并运行常规消息循环。您可以调整窗口大小并移动窗口以查看 GUI 是否未被阻止。当子进程结束时,系统异步调用等待回调,该回调将应用程序定义的消息(WM_APP)发布到窗口。窗口收到消息后,立即调用UnregisterWait()取消等待。如参考所述,即使是使用 WT_EXECUTEONLYONCE 的等待操作也必须在等待完成时取消(但不能从回调中取消!)。然后窗口显示一个消息框,表明它已收到消息。

为简洁起见,省略了错误处理。您应该检查每个 API 函数的返回值,并在返回 FALSE 时调用 GetLastError()

#pragma comment(linker, "/SubSystem:Windows")
#include <windows.h>
#include <string>

int APIENTRY wWinMain( HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPWSTR lpCmdLine, int /*nCmdShow*/ )
{
    if ( wcsstr( lpCmdLine, L"/child" ) )
    {
        MessageBoxW( nullptr, L"Hello from child process!", L"Child", MB_OK );
        return 0;
    }

    // Create window

    struct WindowData
    {
        HANDLE hWait = nullptr;
    }
    wndData;

    WNDCLASSW wc{};
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor( nullptr, IDC_ARROW );
    wc.hbrBackground = reinterpret_cast<HBRUSH>(GetStockObject( WHITE_BRUSH ));
    wc.lpszClassName = L"MyClass";
    wc.cbWndExtra = sizeof(LONG_PTR);
    wc.lpfnWndProc = []( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
    {
        switch ( message )
        {
        case WM_APP:
            {
                // When the wait is completed, you must call the UnregisterWait or UnregisterWaitEx function to cancel 
                // the wait operation. (Even wait operations that use WT_EXECUTEONLYONCE must be canceled.) 
                WindowData* pWndData = reinterpret_cast<WindowData*>(GetWindowLongPtr( hWnd, 0 ));
                UnregisterWait( pWndData->hWait );
                pWndData->hWait = nullptr;

                MessageBoxW( hWnd, L"Child process has ended!", L"Main", MB_OK );
            }
            break;
        case WM_DESTROY:
            PostQuitMessage( 0 );
            break;
        }
        return DefWindowProc( hWnd, message, wParam, lParam );
    };
    RegisterClassW( &wc );

    HWND hWnd = CreateWindowExW( 0, wc.lpszClassName, L"Main", WS_OVERLAPPEDWINDOW | WS_VISIBLE,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr );

    SetWindowLongPtr( hWnd, 0, reinterpret_cast<LONG_PTR>( &wndData) );

    // Create child process
    std::wstring cmd( MAX_PATH, L'\0' );
    cmd.resize( GetModuleFileNameW( nullptr, &cmd[0], cmd.size() ) );
    cmd = L"\"" + cmd + L"\" /child";
    STARTUPINFOW si{ sizeof( si ) };
    PROCESS_INFORMATION pi{};
    CreateProcessW( nullptr, &cmd[0], nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi );

    // Get notified when child process ends
    RegisterWaitForSingleObject( &wndData.hWait, pi.hProcess,
        []( PVOID lpParameter, BOOLEAN /*TimerOrWaitFired*/ )
        {
            PostMessage( reinterpret_cast<HWND>(lpParameter), WM_APP, 0, 0 );
        },
        reinterpret_cast<PVOID>(hWnd), INFINITE, WT_EXECUTEONLYONCE );

    // Run message loop
    MSG msg;
    while ( GetMessage( &msg, nullptr, 0, 0 ) )
    {
        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }

    // Cleanup
    if( wndData.hWait )
        UnregisterWait( wndData.hWait );
    if( pi.hProcess )
        CloseHandle( pi.hProcess );
    if( pi.hThread )
        CloseHandle( pi.hThread );

    return 0;
}

Bonus OldNewThing readWhy bother with RegisterWaitForSingleObject when you have MsgWaitForMultipleObjects?

【讨论】:

    【解决方案2】:

    好消息! Windows 拥有您正在寻找的 API:MsgWaitForMultipleObjects ()

    Tricker,是将其放入 MFC 的消息泵中,但我发现 this link 建议执行以下操作(代码未经测试,已修复(!),并且适合仅等待一个句柄):

    // virtual
    BOOL CMyApp::PumpMessage()
    {
        DWORD const res = ::MsgWaitForMultipleObjects
            (1, &handle_I_am_interested in, TRUE, INFINITE, QS_ALLINPUT);
    
        switch (res)
        {
            case WAIT_OBJECT_0 + 0:
                // the handle was signalled, strut your stuff here
                return TRUE;
    
            case WAIT_OBJECT_0 + 1:
                // there is a message in the queue, let MFC handle it
                return __super::PumpMessage();
        }
    
        // Shouldn't happen
        return TRUE;
    }
    

    我不得不说这段代码对我来说仍然不理想,但它可能已经足够接近了。我对 MFC 的了解不够多,无法进一步评论。

    请注意:在 MFC 通过消息泵之前,此代码不会看到句柄已发出信号。例如,当MessageBox() 拥有控制权时,这可能会发生。如果这让您感到困扰,请考虑改用 RegisterWaitForSingleObject,正如上面传奇的 Raymond Chen 所推荐的那样。

    【讨论】:

    • 看起来确实很有趣,尤其是 PumpMessage 部分。
    • “好消息!Windows 拥有您正在寻找的 API” - 确实如此。它被称为RegisterWaitForSingleObject。或者,使用捕获当前线程上下文的协程,切换到后台线程执行操作并等待,然后切换回初始线程以触发 UI 更新。
    • @IInspectable 使用协程捕获当前线程上下文,切换到后台线程执行操作并等待,然后切换回初始线程触发UI更新 i> 在代码中查看这个会很有趣(协程 - 这是什么?纤维?直接意义上的线程上下文或?直到协程等待才理解 - ui线程将等待消息?)。但是我认为使用 MsgWaitForMultipleObjectsEx 并为 mfc 实施 PumpMessage - 最好的解决方案
    • 协程是 experimental C++ feature,在 Visual Studio 中实现。
    • 如果模态对话框运行 - CWnd::RunModalLoop -> AfxPumpMessage -> pThread-&gt;PumpMessage - PumpMessage 将被调用甚至这种情况。但即使线程启动了一些MessageBox,它们有自己的模态循环——那又如何?关闭后 - 将被称为主消息循环,当进程句柄发出信号时我们会收到通知。此解决方案在任何情况下都不会失败
    【解决方案3】:

    如果你可以修改子进程的代码,你可以只添加一个反向通道,它会在父进程即将离开时通过SendMessage通知父进程。如果你不能这样做,你可以创建一个中继进程,它只会传递原始子进程数据(如果有的话),但会在孩子离开时完成信息工作。当然,这至少可以说远不如仅使用专用线程和例如WaitForSingleObject.

    【讨论】:

    • 子进程是第 3 方,所以不是一个真正的选择,但有趣的想法。
    • @doron 然后你仍然可以创建另一个你可以控制的进程,正如我解释的那样。它将像线程一样工作,它将 CreateProcess、WaitForSingleObject 然后将 SendMessage 发送到您的原始进程。只是一个中继。
    • 是的,你可以这样做,或者任何数量的复杂解决方案。您也可以直接使用RegisterWaitForSingleObject,然后收工。
    【解决方案4】:

    我会做以下两个之一:

    1. 调用 WaitForSingleObject() 或在某处的消息循环中超时为零(可能必须将循环更改为 PeekMessage() 或添加 WM_TIMER 消息以确保您经常检查,)

    2. 或者更好的是,生成一个堆栈非常小的线程(您可以在 CreateThread() 调用中自定义),它只等待这个子进程,然后将消息发布到您的消息循环。

    我更喜欢选项 2,因为具有小堆栈的线程除了等待之外什么都不做,几乎不会消耗资源。

    【讨论】:

    • 这是一个 MFC 应用程序;您无权访问消息循环,因此无法对其进行任何调用。此外,轮询是错误的,尤其是当有立即可用的非轮询解决方案时:MsgWaitForMultipleObjects。但是,这也不适用,因为 MFC。
    • @IInspectable,我不知道该功能,但实际上可以将轮询添加到 MFC。最后的手段只是添加一个计时器(超时为 1 秒或其他时间)并轮询该消息。但我同意投票;最好生成一个小堆栈线程。
    【解决方案5】:

    解决方案是在创建应用时创建一个线程。然后,您等待需要时应触发的事件。示例:

    BOOL bStatus = TRUE;
    CEvent mEvevnt;
    
    // thread function
    UINT LaunchThread( LPVOID p )
    {
        while(bStatus && ::WaitForSingleObject(HANDLE(mEvevnt), INFINITE) == WAIT_OBJECT_0) {
                // create procees here
        }
    }
    
    // thread creation
    AfxBeginThread(LaunchThread, NULL);
    

    触发线程执行:

    mEvevnt.PulseEvent();
    

    您在应用程序结束时销毁线程:

    bStatus = FALSE;
    mEvevnt.PulseEvent();
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-04-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多