可重入函数与线程安全的区别和联系

1.关于可重入函数

当捕捉到信号时,不论进程的主控制流程当前执行到哪,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得整个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突。

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

  可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。

2.线程安全

  一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。

  比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。

单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;

而如果是在多线程情况下,比如有两个线程,线程 A 先将元素1存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B向此 ArrayList 添加元素2,因为此时 Size 仍然等于 0 (注意,我们假设的是添加一个元素是要两个步骤,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值,结果Size等于2。

那好,我们来看看 ArrayList 的情况,期望的元素应该有2个,而 实际元素是在0位置,造成丢失元素,而且Size 等于 2。这就是“线程不安全”了。

3.两者的区别和联系

可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据,也就是不引用静态或全局变量。
可重入函数通常要比不可重入的线程安全函数效率高一些,因为它们不需要同步操作。更进一步说,将第2类线程不安全函数转化为线程安全函数的唯一方法就是重写它,使之可重入。

下面一个函数就是线程安全的但却不可重入的 :

int length = 0;
char *s = NULL;
// Note: Since strings end with a 0, if we want to
// add a 0, we encode it as "\0", and encode a
// backslash as "\\".
// WARNING! This code is buggy - do not use!
void AddToString(int ch)
{
EnterCriticalSection(&someCriticalSection);
// +1 for the character we're about to add
// +1 for the null terminator
char *newString = realloc(s, (length+1) * sizeof(char));
if (newString) {
if (ch == '\0' || ch == '\\') {
AddToString('\\'); // escape prefix
}
newString[length++] = ch;
newString[length] = '\0';
s = newString;
}
LeaveCriticalSection(&someCriticalSection);
}

Linux信号处理函数可重入问题

之前我们的服务程序用到了定时器,最近偶然发现定时器处理函数在执行没完时又一个定时器信号到了。在定时器处理函数中,我们打印了一些信息,打印使用的是某个开源库,这个库在打印时用到了锁功能,Linux下的锁(Mutex)默认是非递归的,也就是一个线程如果已经获得了锁,就不能再次获取到该锁。这样导致的结果是第一次进入定时器处理函数获得锁后,释放之前又一次进入该函数,这时候尝试获取锁失败使得线程在当前位置等待锁,又过了一会第三个定时器信号到达,同样进入处理函数试图取得锁,因为第一次进入获取到了锁而且没有释放,后续的获取锁操作都不会成功,因为定时器处理函数在同一个线程里执行,所以第一次获取到锁后就没法继续执行也就没法释放了,于是产生死锁导致进程崩溃。

信号处理函数属于可重入函数,即在自身没有执行完毕时可以被同一个信号中断,于是又一次进入该函数,因此对于可重入函数,一般

  1. 最好不要执行复杂而耗时的任务;
  2. 不要使用全局变量;
  3. 不要返回指向全局变量的指针;
  4. 如果需要使用全局变量,则先拷贝到局部变量后再使用;
  5. 不要使用不可递归锁甚至最好不用锁;
  6. 不要调用不可重入函数如malloc,调用不可重入函数则该函数自身也变成了不可重入函数。

还记得很久以前在看Linux内核开发资料时,也有“中断处理函数尽可能少做事情”的忠告,时隔多年,还是掉坑里了。

在信号处理函数中,尽量使用可重入的函数接口(即操作的数据都是在堆栈上,不涉及全局变量),否则会引起数据的崩溃。

结束!

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值