您的当前位置:首页正文

可重入性和线程安全性——Reentrancy and Thread-Safety

2024-11-24 来源:个人技术集锦

翻译自 Qt 帮助文档:Reentrancy and Thread-Safety


纵观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,结果是未知的。这是因为 ++ 和 -- 操作符不总是原子的。事实上,它们通常扩展成三个机器指令:

1.将变量的值加载到寄存器中;

2.增加或者减少寄存器的值;

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在其构造函数中自动给mutex上锁,并在函数结尾处调用析构函数时解锁。给mutex上锁保证了不同线程序列化的访问数据。mutex数据成员被声明成mutable型的,是因为我们需要在const函数 value() 中对mutex 上锁和解锁。


Qt 类的小结

许多Qt类是可重入的,但并没有定制成线程安全的,因为将它们定制成线程安全的将会产生额外的开销用来重复的上锁和解锁QMutex。例如,QString是可重入的但非线程安全的。我们可以同时在多个线程中安全的访问QString的不同实例,但是并不能同时在多个线程中安全的访问同一个QString实例(除非使用QMutex保护这些访问操作)。

一些Qt 类和函数是线程安全的。它们主要是线程安全的类(例如,QMutex)和基本的函数(例如,QCoreAQpplication::postEvent() )。

注意:多线程域中的术语不是完全标准化的。POSIX使用可重入和线程安全的定义有点不同于C API的。当在Qt中使用其他的面向对象C++类库时,确保定义能被理解。


显示全文