Lv.的博客

可重入性和线程安全

在本篇文章中,术语"可重入性"和"线程安全"被用来标记类与函数,以表明在多线程应用程序中它们可以被如何使用。
- 一个线程安全的函数可以同时被多个线程调用,甚至这些调用者会使用共享的数据也没有问题,因为对共享数据的访问是串行化的(serialized)。
- 一个可重入函数也可以同时被多个线程调用,但是每个调用者只能使用它自己的数据。
因此,一个线程安全的函数总是可重入的,但一个可重入的函数并不一定是线程安全的。

展开来说,一个可重入的类,指的是它的成员函数可以被多个线程安全地调用,只要每个线程使用这个类的不同的对象。而一个线程安全的类,指的是它的成员函数能够被多线程安全地调用,即使所有的线程都使用该类的同一个实例也没有关系。
注意:有一些Qt的类本来就是被有意地设计为多线程使用的,只有这样的类才在文档中被标明为线程安全的。如果一个函数没有被标记为线程安全的或可重入的,它就不应该被不同的线程使用。如果一个类没有被标记为线程安全的或可重入的,该类的实例就不应该被多个线程访问。

可重入性

C++的类大多是可重入的,这只是因为它们只能访问它们自己的数据。任何线程都能访问一个可重入类的某实例的一个成员函数,只要此时没有其他线程能调用该实例的成员函数。比如,下面的Counter类就是可重入的: 

  1. class Counter
  2.  
    {
  3.  
    public:
  4.  
    Counter() { n = 0; }
  5. void increment() { ++n; }
  6.  
    void decrement() { --n; }
  7.  
    int value() const { return n; }
  8.   
  9. private:
  10.  
    int n;
  11.  
    };

这个类不是线程安全的,因为如果多线程试图修改成员n的话,结果就是不确定的。这是因为++和--操作都不总是原子性的。它们一般被展开为3条机器指令:
1. 将变量值装入寄存器
2. 增或减寄存器中的值
3. 将寄存器中的值装回主存
如果线程A和线程B同时将变量的旧值装入寄存器,增加它们的寄存器,再装回主存,它们最终会互相重写,而变量仅仅被增加了一次!

线程安全

很明显,访问应该是串行的: 线程A必须在无中断的情况下执行完3个步骤(原子性),然后线程B才能开始执行它的步骤,或者反过来。一个使得类是线程安全的简单方法就是用一个QMutex来保护对数据成员的所有访问。

  1.  
    class Counter 
  2. {
  3.  
    public:
  4.  
    Counter() { n = 0; }
  5. void increment() { QMutexLocker locker(&mutex); ++n; }
  6.  
    void decrement() { QMutexLocker locker(&mutex); --n; }
  7.  
    int value() const { QMutexLocker locker(&mutex); return n; } 
  8. private: 
  9. mutable QMutex mutex;
  10.  
    int n;
  11.  
    };

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++的类库一起使用时,要注意定义是明确无歧义的。

posted @ 2018-09-20 09:35  Avatarx  阅读(348)  评论(0编辑  收藏  举报