重入和线程安全
在整个文档中,"重入"和 "线程安全 "这两个术语被用来标记类和函数,以表明它们在多线程应用程序中的使用方式:
- 线程安全函数可以同时被多个线程调用,即使调用使用的是共享数据,因为共享数据的所有引用都已序列化。
- 可重入函数也可以同时被多个线程调用,但前提是每次调用都使用自己的数据。
因此,线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的。
推而广之,如果一个类的成员函数可以被多个线程安全调用,只要每个线程使用的是该类的不同实例,那么这个类就是可重入的。如果可以从多个线程安全地调用类的成员函数,即使所有线程都使用类的相同实例,该类也是线程安全的。
注意: 只有当 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 同时加载变量的旧值、递增寄存器并将其存储回去,那么它们最终会互相覆盖,而变量只会递增一次!
导致类不可重入的典型设计模式
- 使用静态数据成员或全局状态
- 如果类依赖静态变量或全局资源,多个实例或线程共享这些状态时可能引发冲突。
- 单例模式未正确实现线程安全
- 单例类若在初始化时未加锁,多线程可能创建多个实例,破坏单例语义。
- 未保护共享外部资源
- 类若操作文件、数据库连接等外部资源时未加锁,多线程访问会导致资源冲突。
- 依赖非线程安全的第三方库
- 若类封装了非线程安全的第三方 API,直接暴露给多线程环境会导致不可重入。
- 成员函数修改共享内部状态
- 若类的成员函数修改共享的成员变量,且未同步,多线程调用会破坏状态。
- 比如缓存类(不可冲入):多线程调用
add()
可能导致std::map
内部状态损坏。 -
class Cache { private: std::map<std::string, std::string> data_; public: void add(const std::string& key, const std::string& value) { data_[key] = value; // 多线程写入可能破坏 map 结构 } };
线程安全
显然,访问必须序列化:线程 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 类会在构造函数中自动锁定互斥体,并在函数结束调用析构函数时解除锁定。锁定互斥确保来自不同线程的访问将被序列化。mutex
数据成员使用mutable
限定符声明,因为我们需要在value()
中锁定和解锁互斥体,而 是一个常量函数。
Qt 类注意事项
许多 Qt 类都是可重入的,但它们并不是线程安全的,因为如果让它们成为线程安全的,就会产生重复锁定和解锁QMutex 的额外开销。例如,QString 是可重入的,但不是线程安全的。您可以安全地同时从多个线程访问QString 的不同实例,但无法安全地同时从多个线程访问QString 的同一实例(除非您使用QMutex 保护自己的访问)。
有些 Qt 类和函数是线程安全的。这些主要是与线程相关的类(如QMutex )和基本函数(如QCoreApplication::postEvent() )。
注: 多线程领域的术语并不完全标准化。POSIX 使用的可重入和线程安全定义与其 C API 有些不同。在 Qt 中使用其他面向对象的 C++ 类库时,请务必理解这些定义。
Reentrancy and Thread-Safety | Qt 6.8