❝Qt君今天在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
,结果是未定义的。这是因为
++n
和
--n
运算符并不总是原子性的。实际上,它们通常会扩展到这三个机器指令:
- 在寄存器中加载变量的值。
- 寄存器值的递增或递减。
- 将寄存器的值存储回主内存中。
如果线程A和线程B同时加载变量的旧值,增加它们的寄存器,并将其存储回去,它们最终会相互覆盖,造成的后果是变量
n
只增加一次!
线程安全
显然,访问必须是序列化的:线程A必须执行上述步骤123中的原子性不中断,然后线程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类在其构造函数中自动锁定互斥锁,并在调用析构函数时在函数结束时解锁它。锁定互斥锁可以确保来自不同线程的访问将被序列化。互斥锁数据成员使用可变限定符声明的,因为我们需要在value()中锁定和解锁互斥锁,同时它还是一个const修饰的函数。
关于Qt类的注释
「许多Qt类是可重入的,但它们不是线程安全的,因为使它们成为线程安全会导致重复锁定和解锁一个QMutex的额外开销」。例如,QString是可重入的,但不是线程安全的。您可以同时从多个线程安全地访问不同的QString实例,但是不能同时从多个线程安全地访问相同的QString实例(除非您使用QMutex保护自己的访问)。
一些Qt类和函数是线程安全的。这些类主要是与线程相关的类(如QMutex)和基本函数(如QCoreApplication::postEvent())。
「注意」:多线程领域的术语并不是完全标准化的。POSIX使用可重入和线程安全的定义,这与它的C语言API有些不同。在Qt中使用其他面向对象的C++类库时,请确保理解这些定义。