可重入、线程安全辨析与场景举例


定义

       可重入(reentrant)的定义:

       在单个线程中先后执行一段代码是安全的,所谓安全,即一段代码执行的时候,其不会因为进程的signal打断而产生不一致的结果(以及产生的副作用,如更改的全局变量)。signal中断如下:

       线程安全(thread-safe)的定义:

       一个线程内运行的程序不会因为多个线程的运行而产生跟单个线程运行的时候产生不一致的结果或副作用。

       举例:纯函数,即不更改跟全局有关变量的函数;或者是在获取static存储,线程共享变量的时候有加锁的线程。

0.可重入跟线程安全的关系

       可重入程序中可以实现线程安全,但可重入不一定都是线程安全的。反之,线程安全代码也不一定是可重入的(参见下面的示例)。

       一般可重入程序的概念只在signal中断的情况下有讨论意义,并且经常是指单线程(进程)程序中的中断。

1.不可重入/非线程安全

int tmp;

void swap(int* x, int* y)
{
    tmp = *x;
    *x = *y;
    /* Hardware interrupt might invoke isr() here. */
    *y = tmp;   
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}

该函数swap不是thread-safe的,因为tmp可能会因为多线程调用导致一个线程的执行中tmp发生变化,导致*y的值不一致;

也不是reentrant的,因为如其中的注释所说,在中断的时候可以调用isr,在新的isr中如果tmp被赋值,完成调用后回到原线程,则*y的值会不一致;

2.可重入/非线程安全

可重入的函数一般也是线程安全的,但是有很多返利,如下有反例:

int tmp;

int add10(int a) {
  tmp = a;
  return a + 10;
}

该代码仍然有可能在任意地方被signal打断,但是由于在单线程中返回的值与全局共享的tmp无关,所以是reentrant的。但是这个不是thread-safe的,因为tmp在该调用过程中可能被其他线程修改。

3.不可重入/线程安全

thread_local int tmp;
int add10(int a) {
  tmp = a;
  return tmp + 10;
}

该函数是thread-safe的,因为tmp是thread_local变量,各个线程之间的修改不会影响函数结果;

但是它不是reentrant的,因为tmp可能在tmp+10处因为signal中断导致返回的结果发生变化(在同一个线程内)。

4.可重入/线程安全

int add10(int a) {
  return a + 10;
}

该代码是thread-safe的:因为它不涉及多线程内变量的干扰;

是reentrant的:因为它的变量即使被打断了,a的值也不变,返回的结果仍然是a+10。

实际场景

1.signal中断的可重入

       实际代码中signal中断如果是自己写的,那么要避免产生关联函数内互相干扰状态的问题;如果是跟别人的代码一起工作的,那也要注意避免写到全局变量;

2. malloc

       malloc函数因为是对全局内存进行分配的,所以是不可重入的;但是一般对malloc的实现默认是线程安全的。

3. 标准I/O库的不可重入

  标准I/O函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。例如,书上或网上一些例子,信号处理函数中调用了printf,仅仅是为了直观说明程序的运行,实际生产代码中printf不能在信号处理函数中调用。

4.如何保证线程安全性

       避免访问并修改全局变量,static变量,如果要访问,使用mutex保证线程安全;

5. 有一些函数虽然不要求thread-safe,但是也有thread-safe的实现,一般只把它们当作not thread-safe的;

clib中的有些函数是not reentrant的,但是也有reentrant的版本,如rand_r, srand_r,如下:

总结

1.signal可重入和递归调用的区别:

       signal可重入是在任意的一个位置因为信号而被切换出去的,在堆栈上没有直接关联;而递归调用是在确定的位置调用代码的,堆栈有父子关系。

2.

可重入不一定线程安全,线程安全不一定可重入;

但是实际遇到的情况:

程序可重入->大概率线程安全;

线程安全->跟可重入没有太大关系,因为可重入是只针对单个线程内的信号的,跟线程的干扰不一样;而且实际编码中signal内的处理函数是自己写的,只要不写出干扰了全局状态的代码,或者加了自身的非reentrant锁,一般也不会出现问题。

References

[1]Reentrancy, Wikipedia https://en.wikipedia.org/wiki/Reentrancy_(computing)

[2]Is Malloc Thread-Safe? https://stackoverflow.com/questions/855763/is-malloc-thread-safe

[3]日常开发笔记总结(六) https://www.52coder.net/post/weeknote-6

[4]可重入與執行緒安全 (reentrant vs thread-safe) Part1,https://magicjackting.pixnet.net/blog/post/113860339

[5]C-Language Use and Implementation of Interfaces https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09_01