Reentrant和线程安全
- 一、概述
- 二、Reentrant
- 三、线程安全
- 四、Qt类的注意事项
一、概述
在整个文档中, Reentrant 和 thread-safe 术语用于标记类和函数,指明如何在多线程应用程序中使用它们:
- 一个 thread-safe的函数可以从多个线程同时调用,即使调用使用了共享数据,因为对共享数据的所有引用都是序列化的。
- 一个 Reentrant 函数也可以从多个线程同时调用Reentrant函数,但前提是每次调用都使用自己的数据。
因此,thread-safe 的函数总是Reentrant的,但 Reentrant 的函数并不总是线程安全的,可以理解Reentrant 是线程不安全的。
扩展一下,如果一个类的成员函数可以在多个线程中安全地调用,只要每个线程使用这个类的不同实例,那么这个类就是Reentrant的。如果类的成员函数可以在多个线程中安全地调用,那么这个类就是线程安全的,即使所有线程都使用这个类的同一个实例。
注意:Qt类只有在被多个线程使用时才被记录为线程安全的。如果函数未标记为线程安全或Reentrant,则不应在不同的线程中使用它。如果类未标记为线程安全或Reentrant,则不应从不同线程访问该类的特定实例。
二、Reentrant
c++类通常是Reentrant的,因为它们只访问自己的成员数据。任何线程都可以调用Reentrant类实例的成员函数,只要其他线程不能同时调用同一个类实例的成员函数。例如,下面的Counter类是Reentrant的:
class Counter
{public:Counter() { n = 0; }void increment() { ++n; }void decrement() { --n; }int value() const { return n; }private:int n;
};
这个类不是线程安全的,因为如果多个线程试图修改数据成员n,结果是未定义的。这是因为 ++ 和 – 操作符并不总是原子的。实际上,它们通常扩展为3条机器指令:
- 将变量的值加载到寄存器中。
- 增加或减少寄存器的值。
- 将寄存器的值存储到主内存中。
如果线程A和线程B同时加载变量的旧值,增加它们的寄存器并存储它,它们最终会相互覆盖,变量只会增加一次!
三、线程安全
显然,访问必须序列化:线程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类会在构造函数中自动锁定互斥量,并在函数末尾调用析构函数时解锁。锁定互斥量可以确保来自不同线程的访问将被序列化。互斥量数据成员使用可变限定符声明,因为我们需要在value()中锁定和解锁互斥量,这是一个const函数。
四、Qt类的注意事项
许多Qt类是Reentrant的,但它们不是线程安全的,因为使它们成为线程安全的会导致重复锁定和解锁QMutex的额外开销。例如,QString是Reentrant的,但不是线程安全的。你可以从多个线程同时安全地访问不同的QString实例,但你不能从多个线程同时安全地访问同一个QString实例(除非你自己使用QMutex来保护访问)。
有些Qt类和函数是线程安全的。这些主要是线程相关的类(例如QMutex)和基本函数(例如QCoreApplication::postEvent())。
- 注意:多线程领域的术语并没有完全标准化。POSIX使用的Reentrant和线程安全的定义与其C api有些不同。当在Qt中使用其他面向对象的c++类库时,请确保理解这些定义。