最近,我问了一个问题,标题为“ malloc线程安全吗?” ,然后在里面问:“ malloc是否可重入?”
我的印象是所有重入者都是线程安全的。
这个假设错了吗?
Answers:
可重入函数不依赖于C库头中公开的全局变量。.例如,在C中以strtok()vs strtok_r()为例。
有些函数需要在某个位置存储“正在进行的工作”,可重入函数允许您在线程自己的存储中而不是在全局中指定此指针。由于此存储是调用函数所独有的,因此可以中断并重新输入(重新进入),并且由于在大多数情况下,此功能不需要执行该函数实现的互斥,因此通常认为它们是线程安全。但是,按定义不能保证。
但是,errno在POSIX系统上的情况略有不同(并且在所有这些如何工作的任何解释中,它都是奇怪的东西):)
简而言之,可重入通常意味着线程安全(如“如果您正在使用线程,请使用该函数的可重入版本”),但是线程安全并不总是意味着可重入(或相反)。当您考虑线程安全时,并发就是您需要考虑的问题。如果必须提供一种锁定和互斥的方法来使用某个函数,则该函数本质上并不是线程安全的。
但是,并非所有功能都需要检查。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;
}
因此,如您所见,让多个线程使用该线程而不进行某种锁定将是一场灾难..但是重新进入没有任何目的。当某些嵌入式平台上动态分配内存成为禁忌时,您会遇到这种情况。
在纯函数式编程中,可重入通常并不意味着线程安全,它取决于传递给函数入口点,递归等的已定义或匿名函数的行为。
更好的“线程安全”方法对于并发访问是安全的,这更好地说明了这一需求。
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;
}
示例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;
}
t = *x
调用swap()
,中断,t
则将被覆盖,从而导致意外结果。
swap(5, 6)
被中断的调用swap(1, 2)
。之后t=*x
,s=t_original
和t=5
。现在,在中断之后,s=5
和t=1
。但是,在第二个swap
返回之前,它将恢复上下文,即t=s=5
。现在,我们回到第一个swap
,t=5 and s=t_original
然后继续t=*x
。因此,该函数确实是可重入的。请记住,每个调用都会获得自己s
分配的堆栈副本。
这取决于定义。例如,Qt使用以下内容:
即使调用使用共享数据,也可以从多个线程同时调用线程安全*函数,因为对共享数据的所有引用都已序列化。
一个可重入函数也可以从多个线程同时调用,但只有当每个调用使用自己的数据。
因此,线程安全功能始终是可重入的,但可重入功能并非始终是线程安全的。
通过扩展,如果可以从多个线程安全地调用其成员函数,则该类称为可重入的,只要每个线程使用该类的不同实例即可。如果可以从多个线程安全地调用其成员函数,则该类是线程安全的,即使所有线程都使用该类的相同实例也是如此。
但他们也警告:
注意:多线程域中的术语尚未完全标准化。POSIX使用可重入和线程安全的定义,这些定义对其C API有所不同。当将其他面向对象的C ++类库与Qt一起使用时,请确保了解定义。