> 文章列表 > 【并发】线程安全与可重入的理解【2023.04.20】

【并发】线程安全与可重入的理解【2023.04.20】

【并发】线程安全与可重入的理解【2023.04.20】

线程安全与可重入

    • 摘要
    • 对线程安全的理解:
    • 可重入的理解:
    • 直接分析下面四类代码。
    • 总结
  • `补充:Qt的资料,Qt认为线程安全一定是可重入的,但可重入的不一定是线程安全的`。

摘要

源于Qt帮助文档中关于QChar的学习。本文线程安全与可重入讨论的都是函数层级。例如:funA可重入,funB非线程安全。阅读本文需对中断有一定的理解。

对线程安全的理解:

  多线程环境下对共享资源的访问有保护的叫线程安全,没保护的叫线程不安全。如何保护共享资源?实际上就是通过把对象资源的访问进行串行化,即形成一定的代码执行顺序,保证对共享资源的有序访问。

可重入的理解:

  个人理解,可重入讨论的函数层级偏底层些。比如我们写的函数,一般应用层不会考虑函数被复用的情况,甚至是函数被递归调用都不必担心,这是因为有函数调用栈可以保证函数调用的有序。但是到了驱动层、内核层就需要加以注意。举一个例子:单线程场景下,系统有一个定时器中断和键盘中断,二者都执行中断函数IsrCommon。定时器触发的IsrCommon执行到一半,键盘也触发了IsrCommon,这就导致了函数被重入,如果IsrCommon中涉及全局变量或直接操作内存地址,则由于中断时机的不确定性,二者执行IsrCommon的时机也是不确定的,程序的执行结果就会不同。那么这个函数就是不可重入的。

直接分析下面四类代码。

代码例子来源:Thread safety https://en.wikipedia.org/wiki/Thread_safety

关于不可重入与线程安全,下面的四组代码讨论的都是全局变量global,如果单线程下中断导致函数被嵌套执行,且global的结果是确定的,则是可重入的。

  1. 不可重入、非线程安全。单线程下,中断会导致isr被重入,中断顺序不同导致global的结果不同,所以函数isr不可重入。多线程对全局变量global未加锁,所以是线程不安全的。
int global;void swap(int* x, int* y)
{global = *x;*x = *y;*y = global;    
}/* 硬件中断服务程序. */
void isr()
{int x = 1, y = 2;swap(&x, &y);
}
  1. 线程安全、不可重入。全局变量thread_local,可以保证多线程下的线程安全。但单线程下中断导致isr被重复进入,因为swap是操作的内存地址,还是存在结果的不确定性,所以是不可重入的。
_Thread_local int thread_local;void swap(int* x, int* y)
{thread_local = *x;*x = *y;*y = thread_local;    
}/* 硬件中断服务程序. */
void isr()
{int x = 1, y = 2;swap(&x, &y);
}
  1. 可重入,非线程安全。单线程下,对global的访问通过临时变量存储初始值,使用完毕后进行了恢复,不管中断顺序如何,global的结果确定,所以是可重入的。多线程下对全局变量的访问没有保护,所以说是非线程安全的。
int global;void swap(int* x, int* y)
{/* Save global variable. */int func_local;func_local = global;global = *x;*x = *y;      /*If hardware interrupt occurs here then it will fail to keep the value of tmp. So this is also not a reentrant example*/*y = global;     /* Hardware interrupt might invoke isr() here. *//* Restore global variable. */global = func_local;
}void isr()
{int x = 1, y = 2;swap(&x, &y);
}
  1. 可重入、线程安全。未使用全局变量,不涉及global,结果肯定是确定的,所以是可重入的。未涉及全局变量,多线程下也是线程安全的。
void swap(int* x, int* y)
{int func_local;func_local = *x;*x = *y;*y = func_local;    /* Hardware interrupt might invoke isr() here. */
}void isr()
{int x = 1, y = 2;swap(&x, &y);
}

总结

  首先要知道这两个概念是不同的。可重入研究的是系统底层的中断函数执行顺序不同导致函数执行结果不同。线程安全研究的是多线程执行顺序对共享资源的并发访问顺序导致的执行结果不同。简言之,结果确定的就是安全的,可重入的。结果不确定的就是不安全、不可重入的。这也是为啥一直强调全局变量、共享的资源访问要格外小心。


补充:Qt的资料,Qt认为线程安全一定是可重入的,但可重入的不一定是线程安全的

Reentrancy and Thread-Safety

Throughout the documentation, the terms reentrant and thread-safe are used to mark classes and functions to indicate how they can be used in multithread applications:

  • A thread-safe function can be called simultaneously from multiple threads, even when the invocations use shared data, because all references to the shared data are serialized.

  • A reentrant function can also be called simultaneously from multiple threads, but only if each invocation uses its own data.

  • Hence, a thread-safe function is always reentrant, but a reentrant function is not always thread-safe.

  • By extension, a class is said to be reentrant if its member functions can be called safely from multiple threads, as long as each thread uses a different instance of the class. The class is thread-safe if its member functions can be called safely from multiple threads, even if all the threads use the same instance of the class.

  • Note: Qt classes are only documented as thread-safe if they are intended to be used by multiple threads. If a function is not marked as thread-safe or reentrant, it should not be used from different threads. If a class is not marked as thread-safe or reentrant then a specific instance of that class should not be accessed from different threads.

    Reentrancy

C++ classes are often reentrant, simply because they only access their own member data. Any thread can call a member function on an instance of a reentrant class, as long as no other thread can call a member function on the same instance of the class at the same time. For example, the Counter class below is reentrant:

C++类只能访问自己的成员变量,就原生的保证了函数只访问自己的资源,也就是可重入。

class Counter
{
public:Counter() { n = 0; }void increment() { ++n; }void decrement() { --n; }int value() const { return n; }private:int n;
};

The class isn’t thread-safe, because if multiple threads try to modify the data member n, the result is undefined. This is because the ++ and – operators aren’t always atomic. Indeed, they usually expand to three machine instructions:
Load the variable’s value in a register.
Increment or decrement the register’s value.
Store the register’s value back into main memory.
If thread A and thread B load the variable’s old value simultaneously, increment their register, and store it back, they end up overwriting each other, and the variable is incremented only once!
Thread-Safety
Clearly, the access must be serialized: Thread A must perform steps 1, 2, 3 without interruption (atomically) before thread B can perform the same steps; or vice versa. An easy way to make the class thread-safe is to protect all access to the data members with a 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;};

The QMutexLocker class automatically locks the mutex in its constructor and unlocks it when the destructor is invoked, at the end of the function. Locking the mutex ensures that access from different threads will be serialized. The mutex data member is declared with the mutable qualifier because we need to lock and unlock the mutex in value(), which is a const function.
Notes on Qt Classes
Many Qt classes are reentrant, but they are not made thread-safe, because making them thread-safe would incur the extra overhead of repeatedly locking and unlocking a QMutex. For example, QString is reentrant but not thread-safe. You can safely access different instances of QString from multiple threads simultaneously, but you can’t safely access the same instance of QString from multiple threads simultaneously (unless you protect the accesses yourself with a QMutex).
Some Qt classes and functions are thread-safe. These are mainly the thread-related classes (e.g. QMutex) and fundamental functions (e.g. QCoreApplication::postEvent()).
Note: Terminology in the multithreading domain isn’t entirely standardized. POSIX uses definitions of reentrant and thread-safe that are somewhat different for its C APIs. When using other object-oriented C++ class libraries with Qt, be sure the definitions are understood.
Synchronizing Threads ◦ Threads and QObjects