从 WTL 和 C++ 中的工作线程更新 CListViewCtrl

Posted

技术标签:

【中文标题】从 WTL 和 C++ 中的工作线程更新 CListViewCtrl【英文标题】:Updating an CListViewCtrl from worker threads in WTL and C++ 【发布时间】:2017-07-05 05:21:51 【问题描述】:

如标题所示,我想将项目添加/删除到从工作线程的 WTL CListViewCtrl 类派生的类中,但总是得到“抛出未处理的异常:读取访问冲突。”

我尝试过 Win32 API PostMessageSendMessage,但是一旦工作线程接触到 CListViewCtrl 的 HWND,我就会遇到同样的异常。

// CListCtrl member function, calling from worker thread
HWND GetHwnd()

    return hwndListCtrl;       // exception here

我试过这个SafeQueue,但是一旦工作线程接触到互斥锁或队列,就会再次出现异常。

// SafeQueue is member variable in CListViewCtrl, created in GUI thread
SafeQueue<T> m_SafeQueue;
. . .
// member function in SafeQueue class, calling from worker thread
void enqueue(T t)

    std::lock_guard<std::mutex> lock(m);  // exception here
    q->push(t);

我尝试使用 newHeapAlloc/LocalAlloc 创建互斥体和队列,但又出现了同样的异常。

我尝试了 Win32 API CreateMutex 但没有运气,从工作线程访问互斥体句柄时出现同样的异常。

当我从 GUI 线程添加项目时它工作正常。

只有当我将 HWND 或互斥体和队列声明为 static/global 时,它才能在工作线程中工作,但我会避免这种情况,因为我想使用此列表控件中的多个实例,并且我更喜欢任何比全局变量更优雅的方式。

我想让这个类可重用,因为我想通过一些修改(更多列,不同颜色)多次使用它。

感谢任何帮助和想法,让我可以完成这项工作。

环境: VS2015社区,WTL/C++和Win10 Pro 64bit

我发现了导致访问冲突异常的问题: 我在 CListViewCtrl 类中将 ThreadProc 回调函数声明为 静态成员函数

// DO NOT USE
// in CListViewCtrl
**static** DWORD WINAPI ThreadProc(LPVOID lp)

. . .


LRESULT OnStartWorkerThread(WORD /*wNotifyCode*/, WORD /*wID*/, HWND . ..)

    DWORD dw;
    ::CreateThread(NULL, 0, this->ThreadProc, NULL, 0, &dw);

一个可行的解决方案:

class CListViewCtrl ...

    // thread-safe queue to store listctrl items to be added later in GUI thread
    SafeQueue<CListCtrlItem<nCols> > m_SafeQueue;  

    // thread ID of the thread in which listctrl was created, saved in OnCreate
    DWORD m_dwGuiTid;

    // . . .

检查是否从 GUI 或任何其他线程调用了 SafeAddItem 函数

    BOOL InvokeRequired()
    
        if (m_GuiTid == ::GetCurrentThreadId())
            return false;

        return true;
    

    // ...

SafeAddItem成员函数可以从GUI和工作线程调用

    void SafeAddItem(CListCtrlItem<nCols> item)
    
        if (!InvokeRequired())
        
            // we are in GUI thread so just add listctrl item "normal" way
            AddItem(item);
            return;
        

     // we are in other thread so enqueue listctrl item and post a message to GUI           
        m_SafeQueue.Enqueue(item);
        ::PostMessage(m_hWnd, WM_ADD_ITEM, 0, 0);
     
    // . . .

PostMessage的消息处理程序,我们在GUI线程中

    LRESULT OnAddItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& bHandled)
    
        CListCtrlItem<nCols> item;
        while (!m_SafeQueue.Empty())
        
            item = m_SafeQueue.Dequeue();
            // we are in GUI thread so we can add list ctrl items normal way
            AddItem(item);
        
        return 1;
    
    // ...

现在我们可以通过这种方式从任何线程添加 listctrl 项目。我将 this 指针传递给 _beginthreadex

中的 ThreadProc
m_ListCtrl.SafeAddItem(item);

【问题讨论】:

【参考方案1】:

这个问题似乎不是关于工作线程的 UI 更新,而是关于工作线程本身的正确使用。

关于进行 UI 更新的危险的 cmets 有足够的数量:它们都是关于潜在的死锁问题。大多数更新涉及发送消息,这是一个阻塞 API 调用。当您从工作线程进行更新并且调用线程被阻塞时,UI 中处理程序的任何尝试同步或以其他方式与工作线程协作都可能导致死锁。解决此问题的唯一方法是在工作线程中准备更新并向 UI 线程发出信号(包括通过发布消息而不是发送消息,就SendMessagePostMessage API 而言)以接管并完成来自 UI 的更新线程。

回到原始问题:您似乎遇到了静态线程过程的问题。 The fourth argument in the CreateThread call is:

lpParameter [输入,可选]

指向要传递给线程的变量的指针。

您拥有它NULL,您通常使用它来将this 值传递给您的线程过程回调。这样您就可以将执行从静态函数传递回您的类实例:

DWORD CFoo::ThreadProc()

    // ThreadProc with proper "this" initialization
    // HWND h = GetHwnd()...

DWORD WINAPI ThreadProc(LPVOID pvParameter)

    return ((CFoo*) pvParameter)->ThreadProc();

LRESULT CFoo::OnStartWorkerThread(WORD /*wNotifyCode*/, WORD /*wID*/, HWND ...)

    DWORD dw;
    ::CreateThread(NULL, 0, this->ThreadProc, (LPVOID) this, 0, &dw);

还请注意,您不应该直接使用CreateThread:您有_beginthreadexAtlCreateThread (related question)。

【讨论】:

感谢您的建议。我知道 SendMessage 是一个阻塞调用,所以我想改用 PostMessage,但由于这是一个非常奇怪的问题,我尝试了所有解决方案来让它工作。我用一个可行的解决方案更新了我的问题。我使用该 SafeQueue 来临时存储 listctrl 项目,因为在 PostMessage wParam 或 lParam 中跨线程发送指针会更加复杂。【参考方案2】:

在 Windows 中,您永远不应该通过工作线程直接修改 GUI 控件。在 .NET 世界中,如果我们想通过工作线程更新控件,我们必须对基本上执行上下文切换的委托进行平台调用。

你在 WIN32 也有类似的问题。

有一篇关于这个主题的优秀文章我会提请你注意。它还讨论了各种安全的解决方法: https://www.codeproject.com/Articles/552/Using-Worker-Threads

工作线程和 GUI II:不要触摸 GUI

“没错。工作线程不能接触GUI对象。这意味着您不应该查询控件的状态、向列表框添加内容、设置控件的状态等。

为什么?

因为您可能会陷入严重的僵局。一个经典的例子贴在一个讨论板上,它描述了去年发生在我身上的事情。情况是这样的:你启动一个线程,然后决定等待线程完成。同时,该线程执行了一些看似无害的操作,例如向列表框添加内容,或者在发布的示例中调用 FindWindow。在这两种情况下,进程都会突然停止,因为所有线程都死锁了。”

【讨论】:

感谢您的回复。我知道我不应该从其他线程访问 GUI 对象,但我什至无法访问对线程之间的同步很重要的互斥体句柄。并且从其他线程访问数据成员不应该导致访问冲突异常,只是奇怪的行为。我感觉这是一个奇怪的问题,看起来我发现了问题:我在 CListViewCtrl 中将工作线程回调函数声明为静态成员函数。而静态成员函数只能访问静态数据成员。 感谢您的澄清。根据您的描述-“我想将项目添加/删除到从工作线程的 WTL CListViewCtrl 类派生的类中......我尝试了 Win32 API PostMessage 和 SendMessage,但是一旦工作线程触及 CListViewCtrl 的 HWND,我就会得到同样的异常。 .. 当我从 GUI 线程添加项目时,它工作正常。”听起来您正试图直接从工作线程访问 ListView。 很抱歉,如果是误解。首先,我尝试使用 PostMessage,但它的第一个参数是目标窗口 HWND,但我无法从工作线程读取/获取此 HWND,因为它导致访问冲突异常。这对我来说听起来很奇怪,因为这不应该引起异常。现在它可以使用非成员非静态 ThreadProc 函数正常工作。谢谢 感谢您解释您的解决方案。

以上是关于从 WTL 和 C++ 中的工作线程更新 CListViewCtrl的主要内容,如果未能解决你的问题,请参考以下文章

将 WTL 更新到 WTL 10:'AtlCopyBitmap':找不到标识符

如何将 WTL 和 ATL 添加到 Visual Studio C++ Express 2008

WTL强制资源从非mfc应用程序中的dll加载的方式? (我们使用的是 WTL/ATL,不是直接的 win32)

WTL 拆分器示例中的调试断言

C++ 中的游戏更新(线程或计时器)?

将WTL应用向导添加到VS2019