跳转到内容
新建笔记

Qt 线程同步:竞态、互斥锁与等待条件

同步工具保护的是共享状态及其访问协议。给某一个函数加锁,并不能使其他未加锁的读写自动安全;也不能让 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 可重入性与线程安全