最近,我问了一个问题,标题为“malloc线程安全吗?” ,在里面我问,“malloc 是可重入的吗?”
我的印象是所有可重入都是线程安全的。
这个假设是错误的吗?
TL;DR:一个函数可以是可重入的、线程安全的,或者两者都不是。
关于线程安全和重入的维基百科文章非常值得一读。以下是一些引文:
如果满足以下条件,则函数是线程安全的:
它仅以保证多个线程同时安全执行的方式操作共享数据结构。
如果满足以下条件,函数是可重入的:
它可以在其执行期间的任何时候被中断,然后在其先前的调用完成执行之前再次安全地调用(“重新进入”)。
作为可能的重入示例,Wikipedia 给出了一个设计为由系统中断调用的函数的示例:假设在另一个中断发生时它已经在运行。但是不要仅仅因为你没有使用系统中断来编写代码就认为你是安全的:如果你使用回调或递归函数,你可能会在单线程程序中遇到重入问题。
避免混淆的关键是可重入是指只执行一个线程。这是从不存在多任务操作系统时的概念。
例子
(根据维基百科的文章略有修改)
示例 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;
}
这取决于定义。例如Qt 使用以下内容:
可以从多个线程同时调用线程安全* 函数,即使调用使用共享数据也是如此,因为对共享数据的所有引用都是序列化的。
也可以从多个线程同时调用可重入函数,但前提是每次调用都使用自己的数据。
因此,线程安全函数总是可重入的,但可重入函数并不总是线程安全的。
通过扩展,如果一个类的成员函数可以从多个线程安全地调用,只要每个线程使用该类的不同实例,则称该类是可重入的。如果可以从多个线程安全地调用该类的成员函数,则该类是线程安全的,即使所有线程都使用该类的相同实例。
但他们也警告说:
注意:多线程领域的术语不是完全标准化的。POSIX 使用与其 C API 有所不同的可重入和线程安全定义。在 Qt 中使用其他面向对象的 C++ 类库时,请确保理解这些定义。
可重入函数不依赖于 C 库头文件中公开的全局变量。以 C 中的 strtok() 与 strtok_r() 为例。
有些函数需要一个地方来存储“正在进行的工作”,可重入函数允许您在线程自己的存储中指定这个指针,而不是在全局中。由于此存储是调用函数独有的,因此它可以被中断并重新进入(重入),并且由于在大多数情况下,超出函数实现的互斥不是此工作所必需的,因此它们通常被认为是线程安全。然而,这并不能通过定义来保证。
然而,errno 在 POSIX 系统上的情况略有不同(并且在任何解释这一切如何工作时往往是奇怪的):)
简而言之,可重入通常意味着线程安全(如“如果您正在使用线程,请使用该函数的可重入版本”),但线程安全并不总是意味着可重入(或相反)。当您查看线程安全时,您需要考虑并发性。如果您必须提供一种锁定和互斥的方法来使用函数,那么该函数本质上不是线程安全的。
但是,并非所有功能都需要检查。malloc()
不需要可重入,它不依赖于任何给定线程的入口点范围之外的任何内容(并且本身是线程安全的)。
如果不使用 mutex、futex 或其他原子锁定机制,则返回静态分配值的函数不是线程安全的。然而,如果它们不会被打断,它们就不需要可重入。
IE:
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;
}
因此,如您所见,让多个线程在没有某种锁定的情况下使用它将是一场灾难......但它没有可重入的目的。当动态分配内存在某些嵌入式平台上是禁忌时,您会遇到这种情况。
在纯函数式编程中,可重入通常并不意味着线程安全,它取决于传递给函数入口点的已定义或匿名函数的行为、递归等。
放置“线程安全”的更好方法是对并发访问安全,这更好地说明了需求。