Reentrancy and Thread-Safety
在 Qt 的文档中, 术语 可重入 和 线程安全 用于标记类和函数, 如何在多线程应用程序中使用它们:
- 即使使用共享数据, 也可以从多个线程同时调用线程安全函数, 因为对共享数据的所有引用都是序列化的.
- 可重入函数也可以从多个线程同时调用, 但前提是每次调用都使用自己的数据.
因此, 线程安全函数始终是可重入的, 但可重入函数并不总是线程安全的.
综上所述, 如果一个类的成员函数可以从多个线程安全地调用, 只要每个线程使用该类的不同实例, 则该类被认为是可重入的. 如果可以从多个线程安全地调用该类的成员函数, 则该类是线程安全的, 即使所有线程都使用该类的同一实例.
注意: Qt 类仅在供多个线程使用时才被记录为线程安全的. 如果函数未标记为线程安全或可重入, 则不应在不同线程中使用它. 如果一个类未标记为线程安全或可重入, 则不应从不同的线程访问该类的特定实例.
可重入
C++ 类通常是可重入的, 因为它们只访问自己的成员数据. 任何线程都可以调用可重入类的实例上的成员函数, 只要没有其他线程可以同时调用该类的同一实例上的成员函数. 例如, 下面的 Counter
类是可重入的:
class Counter { public: Counter() { n = 0; } void increment() { ++n; } void decrement() { --n; } int value() const { return n; } private: int n; };
该类不是线程安全的, 因为如果多个线程尝试修改数据成员 n
, 结果是未定义的. 这是因为 ++
和 --
运算符并不总是原子的. 事实上, 它们通常扩展到三个机器指令:
- 将变量的值加载到寄存器中.
- 增加或减少寄存器的值.
- 将寄存器的值存储回主存储器.
如果线程 A 和线程 B 同时加载变量的旧值, 递增它们的寄存器, 然后将其存储回来, 它们最终会互相覆盖, 并且变量只会递增一次!
Thread-Safety
显然, 访问必须串行化: 线程 A 必须不间断 (原子地) 执行步骤 1, 2, 3, 然后线程 B 才能执行相同的步骤; 或相反亦然. 使类线程安全的一个简单方法是使用 QMutex 保护对数据成员的所有访问:
class Counter { public: Counter() { n = 0; } void increment() { QMutexLocker locker(&mutex); ++n; } void decrement() { QMutexLocker locker(&mutex); --n; } int value() const { QMutexLocker locker(&mutex); return n; } private: mutable QMutex mutex; int n; };
QMutexLocker 自动在其构造函数中锁定互斥体, 并在函数结束时调用析构函数时将其解锁. 锁定互斥体可确保来自不同线程的访问将被序列化. 互斥体数据成员使用 mutable
限定符声明, 因为我们需要在 value() 中锁定和解锁互斥体, 这是一个 const 函数.
备注
许多 Qt 类是可重入的, 但它们并不是线程安全的, 因为使它们线程安全会产生重复锁定和解锁 QMutex 的额外开销. 例如, QString 是可重入的, 但不是线程安全的. 你可以同时从多个线程安全地访问 QString 的不同实例, 但不能同时从多个线程安全地访问 QString 的同一实例 (除非你使用 QMutex 保护访问).
一些 Qt 类和函数是线程安全的. 这些主要是与线程相关的类 (如. QMutex) 和基本函数 (如. QCoreApplication::postEvent()).
注意: 多线程领域的术语并未完全标准化. 使用的可重入和线程安全的定义与其 C API 有所不同. 当在 Qt 中使用其他面向对象的 C++ 类库时, 请确保理解定义.