Non-reentrant Functions in C++

C语言标准库中,不可重入(Non-reentrant)的函数通常是指那些使用了内部静态变量、全局数据结构、或者返回指向静态内存的指针的函数。在多线程环境或信号处理函数中调用这些函数可能会导致数据竞争或不确定的行为。

以下是一些常见的不可重入的C标准库函数类别及具体示例:

  1. 字符串处理函数:

    • strtok(): 这是最经典的不可重入函数。它内部维护一个静态指针来记住上次分割的位置。在多线程或信号处理中并发调用会导致混乱。

    替代方案 (POSIX): strtok_r() (可重入版本)。

  2. 时间/日期转换函数:

    • asctime(): 返回一个指向内部静态缓冲区的字符串指针。
    • ctime(): 同上,返回一个指向内部静态缓冲区的字符串指针。
    • gmtime(): 返回一个指向内部静态 struct tm 结构的指针。
    • localtime(): 同上,返回一个指向内部静态 struct tm 结构的指针。

    替代方案 (POSIX): asctime_r(), ctime_r(), gmtime_r(), localtime_r() (可重入版本,需要提供缓冲区)。

  3. 随机数生成函数:

    • rand(): 使用一个全局的种子状态。
    • srand(): 修改全局的种子状态。

    替代方案 (POSIX): rand_r() (可重入版本,需要提供种子指针)。

  4. 环境变量函数:

    • getenv(): 某些实现可能会返回指向内部静态缓冲区的指针,

    注意: POSIX 规定 getenv() 是线程安全的,但在信号处理函数中仍然可能不安全。

  5. 本地化(Locale)函数:

    • setlocale(): 修改全局的本地化设置。
    • localeconv(): 返回指向内部静态数据结构的指针。
  6. 文件I/O函数 (<stdio.h> 中的大部分):

    • printf(), scanf(), fgets(), fputs(), fread(), fwrite(), getchar(), putchar() 等所有基于 FILE* 的函数。

    原因: 这些函数内部通常维护缓冲区和文件指针状态,并且会进行锁定操作来保证线程安全(在现代多线程C库中)。然而,它们通常不是“异步信号安全”(Async-Signal-Safe),这意味着在信号处理函数中调用它们是不安全的,因为它们可能会中断主程序正在进行的I/O操作,导致死锁或状态损坏。

总结:

在多线程编程中,POSIX 标准通常会提供带有 _r 后缀的可重入版本(如 strtok_rlocaltime_r 等),或者要求特定的函数(如 mallocfreepthread_create)是线程安全的。

当你在考虑函数是否可重入时,主要关注以下几点:

  • 是否返回指向静态或全局缓冲区的指针。
  • 是否修改内部静态或全局状态。
  • 是否依赖于外部共享资源(如文件描述符和它们的内部状态)。

对于信号处理函数,除了上述不可重入函数外,许多看起来“安全”的函数(如mallocfree、大多数stdio函数)也都不是异步信号安全的,应避免在信号处理函数中使用。

Related

C++ Variadic Function Templates

可变参模板的英文为Variadic Templates,是C++11标准引入的,可变参模板允许模板定义中含有0到多个模板参数。

C++ Fold Expressions

折叠表达式(Fold Expressions)是C++17标准引入的,引入折叠表达式的主要目的是计算某个值。

Clangd Configuration With Eglot

Eglot是Emacs内置的LSP客户端。Eglot内置已支持大量的LSP服务端。以C++语言为例,Eglot支持clangd和ccls两种服务端模式。