可重入性和线程安全
在本篇文章中,术语"可重入性"和"线程安全"被用来标记类与函数,以表明在多线程应用程序中它们可以被如何使用。
- 一个线程安全的函数可以同时被多个线程调用,甚至这些调用者会使用共享的数据也没有问题,因为对共享数据的访问是串行化的(serialized)。
- 一个可重入函数也可以同时被多个线程调用,但是每个调用者只能使用它自己的数据。
因此,一个线程安全的函数总是可重入的,但一个可重入的函数并不一定是线程安全的。
展开来说,一个可重入的类,指的是它的成员函数可以被多个线程安全地调用,只要每个线程使用这个类的不同的对象。而一个线程安全的类,指的是它的成员函数能够被多线程安全地调用,即使所有的线程都使用该类的同一个实例也没有关系。
注意:有一些Qt的类本来就是被有意地设计为多线程使用的,只有这样的类才在文档中被标明为线程安全的。如果一个函数没有被标记为线程安全的或可重入的,它就不应该被不同的线程使用。如果一个类没有被标记为线程安全的或可重入的,该类的实例就不应该被多个线程访问。
可重入性
C++的类大多是可重入的,这只是因为它们只能访问它们自己的数据。任何线程都能访问一个可重入类的某实例的一个成员函数,只要此时没有其他线程能调用该实例的成员函数。比如,下面的Counter类就是可重入的:
-
class Counter
-
{
-
public:
-
Counter() { n = 0; }
-
void increment() { ++n; }
-
void decrement() { --n; }
-
int value() const { return n; }
-
-
private:
-
int n;
-
};
这个类不是线程安全的,因为如果多线程试图修改成员n的话,结果就是不确定的。这是因为++和--操作都不总是原子性的。它们一般被展开为3条机器指令:
1. 将变量值装入寄存器
2. 增或减寄存器中的值
3. 将寄存器中的值装回主存
如果线程A和线程B同时将变量的旧值装入寄存器,增加它们的寄存器,再装回主存,它们最终会互相重写,而变量仅仅被增加了一次!
线程安全
很明显,访问应该是串行的: 线程A必须在无中断的情况下执行完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,而在其析构函数中解锁。锁定mutex保证了其他线程的访问都将是串行化的。
mutex数据成员被声明为mutable的,这是因为value()是一个const函数,但我们需要在其中lock和unlock这个mutex.
关于Qt类的注意事项
许多Qt的类都是可重入的,但它们不是线程安全的,因为线程安全意味着要为锁定与解锁QMutex增加更多的开销。比如,QString是可重入的,但并不是线程安全的。你能够同时从多个线程访问不同的QString的实例,但你不能同时从多个线程访问QString的同一个实例(除非用QMutex保护访问)。
有些Qt的类和函数是线程安全的。它们主要是线程相关类(比如,QMutex)和一些基本函数(比如,QCoreApplication::postEvent())。
注意: 多线程领域中的术语并不是完全标准化的。POSIX使用的可重入和线程安全的定义和它在C API里面的定义就是有些不同的。当Qt和其他面向对象的C++的类库一起使用时,要注意定义是明确无歧义的。