QT可重入与线程安全

可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。相反,不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。可重入函数要么使用本地变量,要么在使用全局变量时保护自己的数据。
可重入函数:
不为连续的调用持有静态数据。
不返回指向静态数据的指针;所有数据都由函数的调用者提供。
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
如果必须访问全局变量,记住利用互斥信号量来保护全局变量。
绝不调用任何不可重入函数

在整个文档中,术语:「可重入和线程安全」用于标记类和函数,以表示它们如何在多线程应用程序中使用:

  • 「即使在调用使用共享数据时,也可以从多个线程同时调用线程安全的函数,因为对共享数据的所有引用都是序列化的」
  • 「也可以从多个线程同时调用可重入函数,但前提是每次调用都使用自己的数据」

  「因此,线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的」

  引申开来,如果一个类的成员函数可以从多个线程安全地调用,则称该类是可重入的,只要每个线程使用该类的不同实例。如果可以从多个线程安全地调用该类的成员函数,即使所有线程使用该类的同一实例,该类也是线程安全的。

  「注意」: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运算符并不总是原子性的。实际上,它们通常会扩展到这三个机器指令:

  1. 在寄存器中加载变量的值。
  2. 寄存器值的递增或递减。
  3. 将寄存器的值存储回主内存中。

  如果线程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++类库时,请确保理解这些定义。

可重入函数与不可重入函数

在实时系统的设计中,经常会出现多个任务调用同一个函数的情况。如果有一个函数不幸被设计成为这样:那么不同任务调用这个函数时可能修改其他任务调用这个函数的数据,从而导致不可预料的后果。这样的函数是不安全的函数,也叫不可重入函数

相反,肯定有一个安全的函数,这个安全的函数又叫可重入函数。那么什么是可重入函数呢?所谓可重入是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错。

一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括 static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括 static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
编写可重入函数时,若使用全局变量,则应通过关中断、信号量(即P、V操作)等手段对其加以保护。
说明:若对所使用的全局变量不加以保护,则此函数就不具有可重入性,即当多个进程调用此函数时,很有可能使有关全局变量变为不可知状态。

示例:假设 Exam 是 int 型全局变量,函数 Squre_Exam 返回 Exam 平方值。那么如下函数不具有可重入性。

int Exam = 0;  
unsigned int example( int para )   
{   
    unsigned int temp;  
    Exam = para; // (**)  
    temp = Square_Exam( );  
    return temp;  
}

此函数若被多个进程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的进程可能正好被激活,那么当新激活的进程执行到此函数时,将使 Exam 赋与另一个不同的 para 值,所以当控制重新回到 “temp = Square_Exam( )” 后,计算出的temp很可能不是预想中的结果。此函数应如下改进。

int Exam = 0;  
unsigned int example( int para )   
{  
    unsigned int temp;  
    [申请信号量操作] //(1)  加锁  
    Exam = para;  
    temp = Square_Exam( );  
    [释放信号量操作] //     解锁   
    return temp;  
}

申请不到“信号量”,说明另外的进程正处于给 Exam 赋值并计算其平方过程中(即正在使用此信号),本进程必须等待其释放信号后,才可继续执行。若申请到信号,则可继续执行,但其它进程必须等待本进程释放信号量后,才能再使用本信号。

保证函数的可重入性的方法:

1)在写函数时候尽量使用局部变量(例如寄存器、堆栈中的变量);

2)对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。

满足下列条件的函数多数是不可重入(不安全)的:

1)函数体内使用了静态的数据结构;

2)函数体内调用了malloc() 或者 free() 函数;

3)函数体内调用了标准 I/O 函数。

如何将一个不可重入的函数改写成可重入函数呢?把一个不可重入函数变成可重入的唯一方法是用可重入规则来重写它。其实很简单,只要遵守了几条很容易理解的规则,那么写出来的函数就是可重入的:

1)不要使用全局变量。因为别的代码很可能改变这些变量值。

2)在和硬件发生交互的时候,切记执行类似 disinterrupt() 之类的操作,就是关闭硬件中断。完成交互记得打开中断,在有些系列上,这叫做“进入/ 退出核心”。

3)不能调用其它任何不可重入的函数。

4)谨慎使用堆栈。

Linux常见的可重入函数

作者:

喜欢围棋和编程。

 
发布于 分类 编程标签

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注