0%

线程安全和可重入函数

今天看到一篇文章中提高 malloc 是线程安全的,但是不可重入。对这方面以前关注不多,因此找了一些资料,记录一下。

线程安全

首先明确线程不安全的原因,一般来说线程不安全主要是因为”共享数据”,那仫什仫是共享数据呢?主要有以下几个方面:

  • 函数的返回值被一个全局变量所接收
  • 由调用者传入的线程间共享的指针变量或者引用变量
  • 函数内部本来就会使用的共享静态变量

通常来说,多线程是为了在同一时间内能够处理更多的同样类型的事情,但是线程不安全却阻碍了我们达到我们的目的。所以,我们有的时候不得不想方设法的把线程不安全的函数改写成线程安全的。也就是虽然会发生这些情况,但是我们也可以通过互斥锁等机制达到线程安全的目的,将一个不安全的线程函数变得安全。

而对于线程安全,我们可以简单的认为就是多线程调用的结果不会发生改变。

一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。

可重入函数

首先了解一下什么叫做函数的重入:

如果一个函数被不同的执行流程调用,就有可能在上一次调用还没有完成时再次进入该函数,这就叫重入。

什么意思呢?比如一个函数有 5 条执行语句,第一个线程调用时执行到第 3 条语句,第二个线程也开始调用了,也就是多个线程同时进入这个函数了,发生了函数的重入。

举个 CSDN 上面看到的例子:

函数重入调用

由上图可知当一个函数访问一个全局链表,就有可能因为重入而造成丢失数据(看红色注释),这就叫不可重入函数;相反的,如果一个函数值调用自己的局部变量和函数,则称为可重入函数。确保一个函数是可重入函数应该满足以下几个条件:

  • 不在函数内部使用静态或者是全局变量
  • 不返回静态或全局数据,数据的产生都由调用者提供
  • 尽量使用本地数据,或者通过重新定义变量拷贝全局变量来保护全局变量
  • 不调用不可重入函数

可重入函数又分为显式可重入和隐式可重入:

  • 显式可重入函数:如果所有函数的参数都是传值传递的(没有指针),并且所有的数据引用都是本地的自动栈变量(也就是说没有引用静态或全局变量),那么函数就是显示可重入的,也就是说无论如何调用,我们都可确定它是可重入的。
  • 隐式可重入函数:可重入函数中的一些参数是引用传递(使用了指针),也就是说,在调用线程的时候传递指向非共享数据的指针时,它才是可重入的。

如果满足以下条件则是一定是不可重入的:

  • 函数体内使用了静态的数据结构
  • 通过 malloc 和 free 来申请和释放内存,因为 malloc 是通过全局链表来管理堆的
  • 调用了标准 I/O 库,因为库里存在大多数都是以不可重入的方式使用全局变量或者是静态变量

为什么线程锁不能达到可重入目的呢?想一想互斥两个字的含义吧。

可重入和线程安全的关系

很明显,可重入函数规则更加严格:可重入函数一定是线程安全的,但是线程安全函数不一定可重入。最显著的例子就是本文开头提到的 malloc 了。

我们知道 malloc 在堆上分配内存,而其内部为了效率,维护了一个堆块链表,这个链表是全局静态变量。但是我们也清楚 malloc 的很多实现都是线程安全的,比如 libc 中有非线程安全/线程安全两个版本 malloc 函数,而且编译器会智能帮你选择是不是需要线程安全的 malloc,因为线程安全会降低效率。那么线程安全的 malloc 是怎么做到的呢,就是前面提到的线程互斥锁。