可重入等价于异步信号安全。

线程安全与可重入以及异步信号安全没有必然联系。

可重入函数

当一个函数可以在任意时刻被中断然后操作系统调度执行另一段代码,这段代码中又调用了该函数而不会出错,则称该函数为可重入(reentrant)的

与并发执行的线程安全不同,可重入强调对单一线程执行时重新进入同一个函数仍然是安全。

可重入概念是在单线程操作系统时提出的

函数的重入可能是由于自身原因,比如执行了jmpcall(跳转或递归)也可能是由于信号的中断处理,比如被UNIX的中断信号处理程序调用。因此可重入也可称作为“异步信号安全”。

可重入函数满足以下条件:

  1. 不使用全局或静态变量。
  2. 不返回全局或静态变量地址。
  3. 只处理有调用者提供的数据。
  4. 调用的函数也是可重入的(malloc函数是不可重入的)。

上述条件的目的是要求可重入函数使用的所有变量都保存在调用堆栈的当前函数栈上,因为当同一执行线程重入执行该函数时加载了新的函数帧,与前一次执行该函数时使用的函数帧不冲突、不相互覆盖,从而保证了可重入执行安全。

I/O代码通常不是可重入的,因为它们依赖于共享资源。

异步信号安全是可重入的一个特殊例子,在可重入的实际情况中除了异步信号处理导致的原因之外还有递归、跳转等。

异步信号安全

当进程捕获到一个信号并进行处理时,进程正常执行的指令序列会信号处理程序暂时中断。

此时会先执行该信号处理程序中的指令,直到从信号处理程序返回,再继续执行捕获信号之前的进程正常执行的指令序列

由于信号是异步的,进程并不清楚什么时候会捕获到信号,因此捕获到信号时进程执行的指令位置是随机的。

如果进程正在执行malloc函数从堆上分配内存,而此时收到信号中断了malloc函数,开始执行其信号处理程序,在该信号处理程序中又调用malloc函数,那么会发生什么?

众所周知,getpwnam函数会将返回结果放置到一个全局静态指针中,那么当进程调用getpwnam函数时被信号中断,转而调用信号处理程序,并在其中又调用了getpwnam函数,那么从信号处理程序退出之后,getpwnam函数的返回值是否正确呢?

异步信号安全函数是在信号处理函数中可以安全调用的函数。具体来说只要是满足可重入函数条件的均为异步信号安全函数。

在信号处理程序中,只能调用异步信号安全函数,以确保程序的正确性和稳定性。

下面是APUE 10.6节提供的异步信号安全函数: 异步信号安全函数

从根本上讲,无论是单线程还是多线程的环境中,只要一个函数在任意时间重入之后不会引起竞争条件,那么就可以认为该函数是可重入的、即异步信号安全的

线程安全

在多线程环境下,如果一个函数可以在相同的时间点可以被多个线程安全地调用,那么该函数就是线程安全的。

为了保证函数的线程安全性,通常需要满足以下条件:

  1. 使用共享资源时需要同步机制保护:线程安全函数在内部应使用适当的同步机制(互斥量、读写锁、条件变量等)来保护共享资源的访问。同步机制可以避免多线程同时访问共享资源时引发的竞争条件。
  2. 尽量不使用全局变量或静态变量:因为这些变量通常会被多个线程使用,可能导致竞争条件。如果使用了全局变量或静态变量,那么在访问这些变量时需要使用同步机制进行保护。
  3. 不调用不可重入以及非线程安全的函数:因为不可重入的函数可能会使用全局变量、静态变量或其他可能导致竞争条件的资源。

示例

下面的例子将getenv()函数改写为线程安全的,但并不能确保是异步信号安全的。

#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>

extern char** environ;

// env_mutex互斥量用于在搜索指定变量时保护环境变量不被修改
pthread_mutex_t env_mutex;

static pthread_once_t init_done = PTHREAD_ONCE_INIT;

static void thread_init()
{
    // 设置递归类型互斥量
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&env_mutex, &attr);
    pthread_mutexattr_destroy(&attr);
}

// 要使getenv_r可重入,需要调用者提供自己的缓冲区
int getenv_r(const char* name, char* buf, int buflen)
{
    int i, len, olen;
    // pthread_once函数的作用是无论多少线程竞争调用getenv_r,确保每个进程仅调用一次thread_init函数
    pthread_once(&init_done, thread_init);
    len = strlen(name);
    // env_mutex互斥量可以使多个线程同步访问共享资源environ,使getenv_r函数变为线程安全的
    // 但是并不能使其对信号处理程序也是可重入的,即使使用了递归互斥量,这也仅仅保证了不会在信号处理程序中不会陷入死锁
    // 因为其调用的pthread函数无法保证是异步信号安全的
    pthread_mutex_lock(&env_mutex);
    for (i = 0; environ[i] != NULL; i++)
    {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
        {
            olen = strlen(&environ[i][len + 1]);
            if (olen >= buflen)
            {
                pthread_mutex_unlock(&env_mutex);
                return (ENOSPC);
            }
            strcpy(buf, &environ[i][len + 1]);
            pthread_mutex_unlock(&env_mutex);
            return 0;
        }
    }
    pthread_mutex_unlock(&env_mutex);
    return (ENOENT);
}

虽然使用env_mutex互斥量保证了多线程之间的同步,并且使用递归互斥量保证了在单线程中重入不会陷入死锁,但是由于pthread函数自身并不能保证是异步信号安全的,因此该函数也不能保证是异步信号安全的。

如果pthread函数是异步信号安全的,那么该函数就可被认为是异步信号安全的,因为重入时加锁可以保证全局变量environ不会被其他线程修改,并且函数内部也没有修改全局变量environ

单线程中重入的方法有异步信号处理程序递归调用跳转等。

区别与联系

  1. 将不可重入函数改为可重入函数需要修改函数接口,使得所有数据都通过函数的调用者提供。将非线程安全函数改为线程安全的,只需要修改函数内部实现,一般是通过加入同步机制以保护共享资源,使之不会被多个线程同时访问。
  2. 可重入是在单线程操作系统背景下,重入的函数按照后进先出的线性次序执行完毕。线程安全的函数的执行时机是由操作系统调度、不可预期的,但是执行该函数的线程会按照CPU时间片轮换执行。
  3. 可重入函数未必是线程安全的,线程安全的函数未必是可重入的。例如函数内部访问加锁访问共享资源,该函数是线程安全但不可重入的。

示例

线程安全、但不可重入

int function()
{
    mutex_lock();
    // ...
    // function body
    // ...
    mutex_unlock();
}

上面的函数在多线程环境下,获得了互斥锁的线程总能获得CPU时间片,向前推进执行进度,最终解开互斥锁,使得其他线程也能获得互斥锁进入临界区。

但是如果在单线程背景下,第一次执行该函数获得互斥锁进入临界区,然后该函数被重入执行,获取互斥锁时就会阻塞并陷入死锁,因为第一次获取互斥锁并进入临界区的执行没有机会获得CPU时间片并解锁。

可重入、非线程安全

int global;

void swap(int* x, int* y)
{
    /* Save global variable. */
    int func_local;
    func_local = global;

    global = *x;
    // 如果在多线程环境下,此时切换到其他线程改变了global的值,就会导致产生线程竞争条件
    *x = *y;
    *y = global;

    /* Restore global variable. */
    global = func_local;
}

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

swap函数只有在单线程操作系统环境中是可重入的,因为第二次执行该函数时,第一次执行已经被中断暂停,而第二次执行结束会恢复全局变量global的状态。

并且在swap函数中没有使用global变量的值,否则global=*x;之后被中断重入global状态就会发生变化,变为不可重入函数。

在多线程操作系统环境下该swap函数是非线程安全的,因为全局变量没有保护,会引发竞争条件。

引用

可重入 - 维基百科,自由的百科全书 (wikipedia.org)

可重入(reentrancy) vs. 线程安全(thread-safety) - 知乎 (zhihu.com)

c - Why is this code reentrant but not thread-safe - Stack Overflow