线程安全与可重入

Posted

技术标签:

【中文标题】线程安全与可重入【英文标题】:Threadsafe vs re-entrant 【发布时间】:2010-10-25 19:16:45 【问题描述】:

最近问了一个问题,题目是"Is malloc thread safe?",里面问的是“malloc re-entrant吗?”

我的印象是所有可重入都是线程安全的。

这个假设是错误的吗?

【问题讨论】:

【参考方案1】:

TL;DR:一个函数可以是可重入的、线程安全的,或者两者都不是。

thread-safety 和 reentrancy 的***文章非常值得一读。以下是一些引文:

如果满足以下条件,函数是线程安全的

它只操作共享数据结构 一种保证多人安全执行的方式 同时线程。

如果满足以下条件,函数是可重入

它可以在执行过程中的任何时候被中断 然后在其之前再次安全地调用(“重新输入”) 之前的调用完成执行。

作为可能的重入示例,Wikipedia 给出了一个设计为由系统中断调用的函数的示例:假设在另一个中断发生时它已经在运行。但不要因为不使用系统中断编写代码就认为自己是安全的:如果使用回调或递归函数,在单线程程序中可能会遇到重入问题。

避免混淆的关键是 reentrant 指的是 只有一个线程在执行。这是一个从那时起的概念 不存在多任务操作系统。

示例

(根据***的文章略有修改)

示例 1:非线程安全,不可重入

/* As this function uses a non-const global variable without
   any precaution, it is neither reentrant nor thread-safe. */

int t;

void swap(int *x, int *y)

    t = *x;
    *x = *y;
    *y = t;

示例 2:线程安全,不可重入

/* We use a thread local variable: the function is now
   thread-safe but still not reentrant (within the
   same thread). */

__thread int t;

void swap(int *x, int *y)

    t = *x;
    *x = *y;
    *y = t;

示例 3:非线程安全,可重入

/* We save the global state in a local variable and we restore
   it at the end of the function.  The function is now reentrant
   but it is not thread safe. */

int t;

void swap(int *x, int *y)

    int s;
    s = t;
    t = *x;
    *x = *y;
    *y = t;
    t = s;

示例 4:线程安全、可重入

/* We use a local variable: the function is now
   thread-safe and reentrant, we have ascended to
   higher plane of existence.  */

void swap(int *x, int *y)

    int t;
    t = *x;
    *x = *y;
    *y = t;

【讨论】:

我知道我不应该仅仅为了表示感谢而发表评论,但这是阐明可重入和线程安全函数之间差异的最佳插图之一。特别是您使用了非常简洁明了的术语,并选择了一个很好的示例函数来区分这 4 个类别。所以,谢谢! 在我看来,示例 3 是不可重入的:如果信号处理程序在 t = *x 之后中断,调用 swap(),那么 t 将被覆盖,导致意外结果。跨度> @SandBag_1996,让我们考虑对swap(5, 6) 的调用被swap(1, 2) 中断。在t=*xs=t_originalt=5 之后。现在,在中断之后,s=5t=1。但是,在第二个swap 返回之前,它将恢复上下文,使t=s=5。现在,我们用t=5 and s=t_original 回到第一个swap 并在t=*x 之后继续。因此,该功能似乎是可重入的。请记住,每个调用都会在堆栈上分配自己的 s 副本。 @SandBag_1996 假设如果函数被中断(在任何时候),它只会被再次调用,我们等到它完成后再继续原来的调用。如果发生其他任何事情,那么它基本上就是多线程,而且这个函数不是线程安全的。假设函数做 ABCD,我们只接受 AB_ABCD_CD,或 A_ABCD_BCD,甚至 A__AB_ABCD_CD__BCD。如您所见,示例 3 在这些假设下可以正常工作,因此它是可重入的。希望这会有所帮助。 @SandBag_1996,互斥锁实际上会使其不可重入。第一次调用锁定互斥锁。第二次调用 - 死锁。【参考方案2】:

这取决于定义。例如Qt uses 如下:

可以从多个线程同时调用线程安全* 函数,即使调用使用共享数据也是如此,因为对共享数据的所有引用都是序列化的。

可重入函数也可以从多个线程同时调用,但前提是每次调用都使用自己的数据。

因此,线程安全函数总是可重入的,但可重入函数并不总是线程安全的。

通过扩展,如果一个类的成员函数可以从多个线程安全地调用,只要每个线程使用该类的不同实例,就称其为可重入。如果可以从多个线程安全地调用该类的成员函数,则该类是线程安全的,即使所有线程都使用该类的相同实例。

但他们也警告:

注意:多线程领域的术语并未完全标准化。 POSIX 使用与其 C API 有所不同的可重入和线程安全定义。在 Qt 中使用其他面向对象的 C++ 类库时,请确保理解这些定义。

【讨论】:

这个可重入的定义太强了。 如果一个函数不使用任何全局 /static 变量,它既是可重入的又是线程安全的。线程 - 安全:当多个线程同时运行您的函数时,是否存在竞争?如果使用 global var,请使用 lock 来保护它。所以它是线程安全的。可重入:如果在你的函数执行过程中出现信号,并且在信号中再次调用你的函数,是否安全???在这种情况下,没有多个线程。最好不要使用任何静态/全局变量来使其可重入,或者像示例 3 中那样。【参考方案3】:

可重入函数不依赖于 C 库头文件中公开的全局变量。以 C 中的 strtok() 与 strtok_r() 为例。

某些函数需要一个地方来存储“正在进行的工作”,可重入函数允许您在线程自己的存储中指定此指针,而不是在全局中。由于此存储是调用函数独有的,因此它可以被中断并重新进入(重新进入),因为在大多数情况下,超出函数实现的互斥不是此工作所必需的,它们通常被认为是线程安全的。然而,这并不能通过定义来保证。

然而,在 POSIX 系统上,errno 的情况略有不同(并且在任何解释这一切如何工作时往往是奇怪的):)

简而言之,可重入经常意味着线程安全(如“如果您正在使用线程,请使用该函数的可重入版本”),但线程安全并不总是意味着可重入(或相反)。当您考虑线程安全时,并发 是您需要考虑的。如果您必须提供一种锁定和互斥的方法来使用函数,那么该函数本质上不是线程安全的。

但是,并非所有功能都需要检查。 malloc() 不需要可重入,它不依赖于任何给定线程的入口点范围之外的任何内容(并且本身是线程安全的)。

返回静态分配值的函数在不使用互斥锁、futex 或其他原子锁定机制的情况下不是线程安全的。然而,如果它们不会被打断,它们就不需要可重入。

即:

static char *foo(unsigned int flags)

  static char ret[2] =  0 ;

  if (flags & FOO_BAR)
    ret[0] = 'c';
  else if (flags & BAR_FOO)
    ret[0] = 'd';
  else
    ret[0] = 'e';

  ret[1] = 'A';

  return ret;

因此,如您所见,让多个线程在没有某种锁定的情况下使用它会是一场灾难……但它没有可重入的目的。当动态分配内存在某些嵌入式平台上是禁忌时,您会遇到这种情况。

在纯函数式编程中,可重入通常并不意味着线程安全,它取决于传递给函数入口点的已定义或匿名函数的行为、递归等。

“线程安全”的更好方式是对并发访问安全,这更好地说明了需求。

【讨论】:

可重入并不意味着线程安全。纯函数意味着线程安全。 很好的答案蒂姆。澄清一下,我对您“经常”的理解是线程安全并不意味着可重入,但可重入也不意味着线程安全。你能找到一个不是线程安全的可重入函数的例子吗? @Tim Post “简而言之,可重入通常意味着线程安全(如“如果您正在使用线程,请使用该函数的可重入版本”),但线程安全并不总是意味着可重入。” qt says 相反:“因此,线程安全函数总是可重入的,但可重入函数并不总是线程安全的。” 和 wikipedia says 另一个:“重入的定义不同于多线程环境中线程安全的定义。重入子程序可以实现线程安全,[1] 但可重入单独可能不足以在所有情况下都是线程安全的。相反,线程安全代码不一定必须是可重入的 (...)" @Riccardo:通过 volatile 变量同步的函数,但不是用于信号/中断处理程序的完整内存屏障,通常是可重入的,但线程安全。

以上是关于线程安全与可重入的主要内容,如果未能解决你的问题,请参考以下文章

线程安全与可重入

线程安全与可重入编写方法

线程安全与可重入函数

线程安全与可重入函数之间的区别

线程安全与可重入函数

对线程安全, 可重入函数, 异步安全的理解