Threadsafe vs re-entrant

Alp*_*neo 84 c thread-safety reentrancy

最近,我问了一个问题,标题是"malloc线程安全吗?" 在里面我问道,"malloc是否重新进入?"

我的印象是所有重入者都是线程安全的.

这个假设是错的吗?

Min*_*ark 69

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

关于线程安全重入的维基百科文章非常值得一读.以下是一些引用:

在以下情况下,函数是线程安全的:

它只能以保证多个线程同时安全执行的方式操作共享数据结构.

在以下情况下,函数是可重入的:

它可以在执行期间的任何时候中断,然后在其先前的调用完成执行之前再次安全地再次调用("重新输入").

作为可能重入的示例,维基百科给出了一个被系统中断调用的函数示例:假设它在另一个中断发生时已经在运行.但是不要因为你没有使用系统中断编码而认为你是安全的:如果使用回调或递归函数,你可以在单线程程序中出现重入问题.

避免混淆的关键是重入只引用一个执行的线程.这是一个从没有多任务操作系统的时候开始的概念.

例子

(从维基百科文章略有修改)

示例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;
}
Run Code Online (Sandbox Code Playgroud)

示例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;
}
Run Code Online (Sandbox Code Playgroud)

示例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;
}
Run Code Online (Sandbox Code Playgroud)

示例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;
}
Run Code Online (Sandbox Code Playgroud)

  • 我知道我不应仅仅表示感谢,但这是解决重入和线程安全功能之间差异的最佳插图之一.特别是你使用了非常简洁的术语,并选择了一个很好的示例函数来区分4个类别.那谢谢啦! (10认同)
  • 在我看来,例如3不可重入:如果信号处理程序在`t =*x`之后中断,则调用`swap()`,那么`t`将被覆盖,从而导致意外结果. (9认同)
  • @SandBag_1996,让我们考虑一下对“swap(5, 6)”的调用被“swap(1, 2)”中断。在`t=*x`之后,`s=t_original`和`t=5`。现在,中断之后,`s=5` 和 `t=1`。然而,在第二个“swap”返回之前,它将恢复上下文,使“t=s=5”。现在,我们回到第一个“swap”,其中“t=5”和“s=t_original”,并在“t=*x”之后继续。因此,该函数确实看起来是可重入的。请记住,每个调用都会在堆栈上分配自己的“s”副本。 (2认同)
  • @ SandBag_1996假设是,如果该函数(在任何时候)被中断,则只能再次调用它,并且我们等待直到完成为止,然后再继续原始调用。如果发生任何其他事情,那么它基本上是多线程的,并且此函数不是线程安全的。假设函数执行ABCD,我们只接受AB_ABCD_CD,A_ABCD_BCD甚至A__AB_ABCD_CD__BCD之类的东西。如您所见,示例3在这些假设下可以正常工作,因此是可重入的。希望这可以帮助。 (2认同)
  • @SandBag_1996,互斥锁实际上会使其不可重入。第一次调用锁定互斥体。第二次调用出现 - 死锁。 (2认同)
  • @SandBag_1996 你说“如果信号处理程序在 t = *x 之后中断,调用 swap(),那么 t 将被覆盖,导致意外的结果” - 是的,'t' 将被覆盖,但这不会影响最终结果交换的结果并且结果将是正确的(即不会有“意外结果”)。 (2认同)

Geo*_*lly 55

这取决于定义.例如,Qt使用以下内容:

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

  • 一个可重入函数也可以从多个线程同时调用,但只有当每个调用使用自己的数据.

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

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

但他们也提醒:

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

  • Downvote.线程安全功能并不总是可重入的. (5认同)
  • 这个可重入的定义太强了。 (3认同)

Tim*_*ost 41

可重入函数不依赖于C库头中公开的全局变量.例如在C中使用strtok()vs strtok_r().

某些函数需要一个位置来存储"正在进行的工作",重入函数允许您在线程自己的存储中指定此指针,而不是在全局存储中.由于这个存储是调用函数独有的,它可以被中断和重新输入(重入),因为在大多数情况下,除了函数实现之外的互斥不需要工作,它们通常被认为是线程安全.但是,这不是由定义保证的.

然而,errno在POSIX系统上是一个略有不同的情况(并且在任何解释这一切都有效的过程中往往是奇怪的):)

简而言之,重入通常意味着线程安全(如"你使用线程时使用该函数的可重入版本"),但线程安全并不总是意味着重入(或反向).当您查看线程安全时,并发性就是您需要考虑的问题.如果必须提供锁定和互斥的方法来使用函数,那么该函数本身并不是线程安全的.

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

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

即:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

放置"线程安全"的更好方法对于并发访问安全的,这更好地说明了需要.

  • 可重入并不意味着线程安全.纯粹的功能意味着线程安全. (2认同)