应避免用 std::promise/std::future 传递多次进度,而应为每次更新创建新 promise-future 对并配合线程安全队列;对简单进度可用 std::atomic+回调;Qt 中必须用 QMetaObject::invokeMethod 转发到主线程更新 UI。

用 std::promise + std::future 传递单次进度快照
异步任务中想“报告进度”,本质是让工作线程能往主线程(或监控线程)写入中间状态。但 std::future 默认只支持一次 set_value(),适合终态;若强行复用会触发 std::future_error: future already satisfied。所以别把它当管道用。
真正可行的做法是:为每次进度更新单独构造一对 std::promise/std::future,并通过线程安全的队列(如 std::queue 加 std::mutex)把 std::future 推给接收方。接收方用 wait_for() 轮询,避免阻塞。
- 发送方每次调用
progress_promise.set_value(42)后,立刻 new 一个新的std::promise<int> - 接收方不要调用
get()——它会阻塞直到完成;改用wait_for(std::chrono::milliseconds(10)) == std::future_status::ready - 注意
std::promise对象不能跨线程拷贝,必须用std::move转移所有权
用 std::atomic + 回调函数实现轻量级实时更新
如果进度只是整数百分比、阶段编号这类简单值,且不需要严格顺序保证(比如允许偶尔跳过中间值),std::atomic<int> 配合用户注册的回调是最轻量的选择。它绕过了内存分配和锁,延迟极低。
关键点在于:回调函数必须是无状态或自带同步的,不能在回调里直接操作 UI 控件(如 Qt 的 QWidget),否则可能引发跨线程访问崩溃。
立即学习“C++免费学习笔记(深入)”;
- 定义
std::atomic<int> current_progress{0}和std::function<void(int)> on_progress_update - 工作线程里每更新一次:先
current_progress.store(new_val, std::memory_order_relaxed),再调用on_progress_update(new_val) - 主线程注册回调时,确保该回调对象生命周期长于工作线程(例如用
std::shared_ptr管理)
用 concurrent_queue(TBB 或 folly)避免手写锁队列
自己用 std::mutex 包裹 std::queue 容易出错:比如忘记在 empty() 后加锁就调用 front(),导致竞态;或者在异常路径中未解锁。工业级项目建议直接用经过充分测试的并发队列。
TBB 的 tbb::concurrent_queue 支持多生产者多消费者,push/pop 都是无锁(lock-free)或细粒度锁,且 API 简洁。folly 的 ProducerConsumerQueue 更激进,完全无锁,但要求元素类型 trivially copyable。
- TBB 示例:声明
tbb::concurrent_queue<int> progress_queue,工作线程调用progress_queue.push(75),主线程用progress_queue.try_pop(val)非阻塞取值 - 别用
size()判断是否有数据——它不保证原子性;始终用try_pop()的返回值判断 - 如果用自定义结构体(如
struct Progress { int pct; std::string msg; }),确保它满足移动语义且不含裸指针
Qt 场景下必须走 QMetaObject::invokeMethod
在 Qt 应用中,任何修改 UI 的操作(包括更新进度条、日志框)都必须在主线程执行。即使你用了 std::atomic 或并发队列,也不能让工作线程直接调用 ui->progressBar->setValue(),否则程序大概率 crash 或行为未定义。
正确做法是:工作线程把进度数据发到主线程对象的消息循环,靠 Qt 的事件机制调度执行。这比手写信号槽更底层、更可控,尤其适合高频更新场景(如每毫秒一次)。
- 主线程对象(如
MainWindow)需继承自QObject,并声明 slot 函数:void updateProgress(int value) - 工作线程中调用:
QMetaObject::invokeMethod(main_window, [=]() { main_window->updateProgress(63); }, Qt::QueuedConnection) -
Qt::QueuedConnection是必须的;用Qt::DirectConnection会绕过事件循环,在工作线程中直接调用 slot,依然危险
进度报告真正的复杂点不在“怎么传数据”,而在于“谁负责清理”和“更新频率与吞吐的平衡”。比如每毫秒上报一次,但 UI 只能每 30ms 刷新一帧,中间大量消息就该被合并或丢弃——这个逻辑得你自己加,标准库和 Qt 都不会替你做。


















