可重入性与线程安全性
在文档中,术语可重入和线程安全用于标记类和函数,以指示它们在多线程应用程序中的使用方式:
- 线程安全的函数可以同时从多个线程调用,即使调用使用了共享数据,因为对共享数据的所有引用都是串行化的。
- 可重入的函数也可以同时从多个线程调用,但前提是每次调用都使用自己的数据。
因此,线程安全的函数始终是可重入的,但可重入的函数并不总是线程安全的。
延伸来说,如果一个类的成员函数可以安全地从多个线程调用,只要每个线程使用该类的不同实例,那么该类被称为可重入。如果一个类的成员函数可以安全地从多个线程调用,即使所有线程使用该类的相同实例,那么该类被称为线程安全。
注意: 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 同时加载变量的旧值,增加其寄存器,然后存储回去,它们最终会互相覆盖,变量仅会增加一次!
线程安全性
显然,访问必须是串行化的:在线程 B 执行相同步骤之前,线程 A 必须无中断地执行步骤 1、2、3;或者反之亦然。使类线程安全的一种简单方法是使用 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()
中锁定和解锁互斥体,而 value()
是一个 const 函数。
关于 Qt 类的注释
许多 Qt 类是可重入的,但它们并不是线程安全的,因为使它们线程安全会带来额外的开销,需要反复锁定和解锁 QMutex。例如,QString 是可重入的但不是线程安全的。您可以从多个线程同时安全地访问不同的 QString 实例,但您不能安全地从多个线程同时访问相同的 QString 实例(除非您自己使用 QMutex 保护访问)。
一些 Qt 类和函数是线程安全的。这些主要是与线程相关的类(例如 QMutex)和基本函数(例如 QCoreApplication::postEvent())。
注意: 在多线程领域,术语并没有完全统一。POSIX 使用了对其 C API 有些不同的可重入和线程安全的定义。当使用其他面向对象的 C++ 类库与 Qt 一起使用时,请确保理解这些定义。