Windows GUI程序的窗口刷新必须在创建窗口的主线程中执行,子线程需通过PostMessage发送自定义消息通知主线程刷新,严禁跨线程调用InvalidateRect等API或直接操作HWND。

主线程必须负责窗口消息循环,不能用 std::thread 直接调用 WinAPI 绘图
Windows GUI 程序的窗口刷新(RedrawWindow、InvalidateRect、UpdateWindow)必须在创建该窗口的线程中执行——也就是主线程(UI 线程)。如果在 std::thread 里直接调用这些函数,调用会静默失败,或触发 0x80070006(访问被拒绝)错误,但不会崩溃,极难排查。
常见错误现象:
- 新开线程里调用
InvalidateRect(hWnd, nullptr, TRUE),窗口完全不重绘 - 用
PostMessage(hWnd, WM_PAINT, 0, 0)也不生效——因为WM_PAINT是由系统发给有消息循环的线程的,子线程没消息泵,收不到
正确做法是:子线程只做耗时计算/数据准备,完成后「通知」主线程刷新。通知方式必须走 Windows 消息机制。
用 PostMessage + 自定义消息让主线程安全刷新
这是最轻量、最兼容的方案。关键点在于:子线程不碰 HWND 或 GDI,只发消息;主线程在 WndProc 中响应并调用绘图逻辑。
立即学习“C++免费学习笔记(深入)”;
实操建议:
- 在头文件或全局定义唯一自定义消息:
#define WM_ASYNC_REFRESH (WM_USER + 101) - 子线程完成工作后,调用
PostMessage(hWnd, WM_ASYNC_REFRESH, 0, reinterpret_cast<lparam>(data_ptr))</lparam>—— 注意用PostMessage(异步),不是SendMessage(同步阻塞,可能死锁) - 主线程的
WndProc中增加分支:case WM_ASYNC_REFRESH: OnAsyncRefresh(reinterpret_cast<void>(lParam)); break;</void> -
OnAsyncRefresh内部调用InvalidateRect或直接RedrawWindow,确保所有 UI 操作都在主线程上下文
注意:lParam 传指针时,需确保子线程中分配的内存生命周期覆盖到主线程处理完消息(例如用 std::shared_ptr 管理,或用队列+原子标志位协调释放)。
std::async + std::future 不适合直接驱动窗口刷新
std::async 启动的任务默认在后台线程运行,但它不解决线程亲和性问题。即使你用 std::future 等待结果,拿到数据后仍需回到主线程才能刷新——而 std::future::wait 本身会阻塞当前线程,若在主线程调用就卡死 UI。
典型误用场景:
- 主线程里写
auto f = std::async(std::launch::async, HeavyCalc); f.wait(); InvalidateRect(...);→ UI 卡住几秒,完全失去“异步”意义 - 子线程里
f.get()后直接绘图 → 同样违反线程规则,无效
所以 std::async 只适合作为子任务封装工具,最终结果仍要靠 PostMessage 或其他线程间通信机制交回主线程。
别碰 SetTimer 或 WM_TIMER 做“伪异步刷新”
有人试图用定时器轮询检查子线程状态,再触发刷新。这不仅低效,还引入竞态:
- 子线程刚写完数据,主线程还没来得及读,定时器就触发了 → 读到脏数据或空指针
- 需要加锁保护共享数据,但
WM_TIMER处理函数里调用EnterCriticalSection风险高(可能重入或影响消息吞吐) - 定时器最小精度约 15ms,对实时性要求高的刷新(如动画帧)不可靠
真需要周期性更新,应让子线程自己控制节奏,每次完成即发一次 WM_ASYNC_REFRESH,主线程按需响应——更准、更省资源。
最容易被忽略的一点:HWND 不是线程安全句柄,任何跨线程使用(包括 IsWindow、GetDC)都未定义行为。只要不确定当前线程就是创建窗口的线程,就别碰它。


















