同步工具保护的是共享状态及其访问协议。给某一个函数加锁,并不能使其他未加锁的读写自动安全;也不能让 QWidget 的跨线程调用变得合法。
选择合适的工具
跳转到“选择合适的工具”| 工具 | 解决的问题 | 典型限制 |
|---|---|---|
| QMutex / QMutexLocker | 一次只允许一个执行者访问临界区 | 读写方必须使用同一锁;避免持锁调用未知代码 |
| QReadWriteLock | 多读者或单写者 | 不能持读锁再直接升级取写锁 |
| QSemaphore | 限制可同时占用的资源数量 | 不自动保护其他容器操作,许可获取/释放须配对 |
| QWaitCondition | 等待由锁保护的条件成立 | 必须用循环重新检查条件 |
| QAtomicInt / std::atomic | 一个原子变量的读写或增减 | 不会自动让一组变量形成原子事务 |
QMutexLocker<QMutex> locker(&mutex); 在作用域开始时加锁,离开时解锁,包括异常路径。Qt 6 中 QMutexLocker 是模板;C++17 也可由参数推导模板类型。手动 lock() / unlock() 更容易在提前返回时漏解锁,应在确有需要时使用。QMutexLocker
旧稿的 QMutexPool 示例不能作为 Qt 6 公共 API 使用。需要多个锁时,应明确每把锁保护的对象和固定加锁顺序,避免从字符串“获取锁”的示意掩盖寿命问题。
完整例子:等待数据或关闭
跳转到“完整例子:等待数据或关闭”下面用一个生产者、一个消费者和有限队列数据展示等待协议。它是命令行示例,最后允许 join 等待线程结束。
// main.cpp;C++17,链接 Qt6::Core#include <QMutex>#include <QMutexLocker>#include <QWaitCondition>#include <cassert>#include <deque>#include <optional>#include <thread>
class Mailbox {public: bool push(int value) { QMutexLocker lock(&mutex_); if (closed_) return false; values_.push_back(value); ready_.wakeOne(); return true; } std::optional<int> pop() { QMutexLocker lock(&mutex_); while (values_.empty() && !closed_) ready_.wait(&mutex_); if (values_.empty()) return std::nullopt; const int value = values_.front(); values_.pop_front(); return value; } void close() { QMutexLocker lock(&mutex_); closed_ = true; ready_.wakeAll(); }private: QMutex mutex_; QWaitCondition ready_; std::deque<int> values_; bool closed_ = false;};
int main() { Mailbox mailbox; int sum = 0; std::thread consumer([&] { while (const auto value = mailbox.pop()) sum += *value; }); std::thread producer([&] { for (int value = 1; value <= 5; ++value) { const bool accepted = mailbox.push(value); assert(accepted); } mailbox.close(); }); producer.join(); consumer.join(); assert(sum == 15); const bool acceptedAfterClose = mailbox.push(6); assert(!acceptedAfterClose); assert(!mailbox.pop().has_value());}wait 调用前必须持有 mutex。等待期间它释放锁,让生产者能修改状态;返回前重新取得锁,然后循环检查条件。第二参数若存在,表示等待期限或时间,不是谓词函数。不要写 condition.wait(&mutex, condition_test) 来模拟其他标准库接口。
wakeOne() 或 wakeAll() 不是可累积的“通知数量”,真正决定是否继续等待的是 values_ 和 closed_。先检查条件再等待,可处理通知先于等待发生的情况;被唤醒后重查也能处理状态被其他消费者改变的情况。关闭时唤醒等待者,才能让无数据的消费者退出。QWaitCondition
读锁和写锁的作用域
跳转到“读锁和写锁的作用域”QReadWriteLock lock;int sharedValue = 0;int snapshot = 0;{ QReadLocker reader(&lock); snapshot = sharedValue;} // 先释放读锁{ QWriteLocker writer(&lock); ++sharedValue;}不能在 reader 仍存活时构造同一把锁的 writer。读锁释放到写锁取得之间,其他线程可能改变状态;若需要“读当前值后计算再写回”的整体操作,应在一开始就取得写锁,不能把旧 snapshot 当作仍然最新。QReadWriteLock
信号量、原子量与线程归属
跳转到“信号量、原子量与线程归属”QSemaphore permits(2); 配合 acquire() / release() 可把并行使用某资源的数量限制为两个;应考虑异常路径和不允许无限等待的情况。QAtomicInt 可用 fetchAndAddRelaxed(1) 做独立计数,但只做计数的原子性并不等于同时发布其他数据;有依赖关系时要选择正确的内存顺序或直接使用锁。QSemaphore、QAtomicInteger
检查 QThread::currentThread() 回答“现在在哪个线程执行”;检查 object->thread() 回答“这个 QObject 归属哪个线程”。二者不同,就不能假定普通成员调用是安全的。
若对象应在其所属线程处理操作,可使用带上下文的队列调用,例如 QMetaObject::invokeMethod(object, [object] { /* 在对象线程处理 */ }, Qt::QueuedConnection);。调用方仍需保证提交时 object 有效,接收线程也须有事件循环;仅把一个方法名保存在 QString 中,直接传给旧的 const char* 重载并不正确。QMetaObject
竞态条件与数据竞争不是同一个概念
跳转到“竞态条件与数据竞争不是同一个概念”竞态条件是业务正确性依赖不受控制的执行顺序。C++ 数据竞争有更具体的含义:不同线程执行未建立先行关系的冲突访问,且至少一个访问不是原子操作;它会导致未定义行为。两个线程对普通 int 无同步地执行 ++counter 属于这种错误,不能保证后果只是“本应加两次,实际只加一次”。不要用偶尔得到正确结果证明它安全。C++ 内存模型
即使每次访问都是原子的,也可能存在业务竞态。例如先检查库存大于零,再单独减一,两个线程可能都通过检查;需要锁住检查与更新的整个操作,或使用正确的 compare-and-swap 循环。多个进程争用文件时还需要相应的进程间协议,单个进程内的 QMutex 不会自动保护别的进程。
下面保留原稿的三种计数方案,并把共享 QVector 的外部锁放在同一程序验证。各块是独立示范,实际项目按需求选择,不需要给同一个计数同时套三种同步工具。
// main.cpp;C++17,链接 Qt6::Core#include <QAtomicInt>#include <QMutex>#include <QMutexLocker>#include <QSemaphore>#include <QVector>#include <cassert>#include <thread>
int main() { QMutex mutex; int lockedCount = 0; QVector<int> values; QSemaphore permit(1); int semaphoreCount = 0; QAtomicInt atomicCount{0}; const auto increment = [&] { for (int i = 0; i < 1000; ++i) { { QMutexLocker guard(&mutex); ++lockedCount; values.append(i); } { permit.acquire(); QSemaphoreReleaser release(&permit); ++semaphoreCount; } atomicCount.fetchAndAddRelaxed(1); } }; std::thread first(increment), second(increment); first.join(); second.join(); assert(lockedCount == 2000); assert(values.size() == 2000); assert(semaphoreCount == 2000); assert(atomicCount.loadRelaxed() == 2000);}QVector 本身不会同步对同一实例的并发修改;外部锁须覆盖所有并发读写,包括迭代、size 查询和修改。上面最后的普通读取发生在两个 join 之后,写入线程已经结束。二元信号量在严格配对的协议下也能限制到一个访问者,但与具有线程所有权要求的互斥量语义不同;资源许可一般更适合用 QSemaphore 表达。QSemaphoreReleaser、Qt 可重入性与线程安全