Threadsafe vs re-entrant

时间:2009-05-13 08:43:14

标签: c thread-safety reentrancy

最近,我问了一个标题为"Is malloc thread safe?"的问题,在里面我问道,“malloc是否重入?”

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

这个假设是错的吗?

3 个答案:

答案 0 :(得分:65)

TL;DR: A function can be reentrant, thread-safe, both or neither.

The Wikipedia articles for thread-safety and reentrancy are well worth reading. Here are a few citations:

A function is thread-safe if:

it only manipulates shared data structures in a manner that guarantees safe execution by multiple threads at the same time.

A function is reentrant if:

it can be interrupted at any point during its execution and then safely called again ("re-entered") before its previous invocations complete execution.

As examples of possible reentrance, the Wikipedia gives the example of a function designed to be called by system interrupts: suppose it is already running when another interrupt happens. But don't think you're safe just because you don't code with system interrupts: you can have reentrance problems in a single-threaded program if you use callbacks or recursive functions.

The key for avoiding confusion is that reentrant refers to only one thread executing. It is a concept from the time when no multitasking operating systems existed.

Examples

(Slightly modified from the Wikipedia articles)

Example 1: not thread-safe, not reentrant

/* 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;
}

Example 2: thread-safe, not reentrant

/* 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;
}

Example 3: not thread-safe, reentrant

/* 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;
}

Example 4: thread-safe, reentrant

/* 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;
}

答案 1 :(得分:55)

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

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

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

  •   
     

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

     

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

但他们也提醒:

  

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

答案 2 :(得分:40)

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

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

然而,对于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;
}

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

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

将“线程安全”放在更好的方法是 可以安全地进行并发访问 ,这更能说明需要。