线程安全函数和可重入函数 辨析
偶然想起之前项目里面遇到的一个操作合成结果翻转导致的画面hung死的问题,一时有点记不清了,后面看了细节之后再补充吧。
反正借着这个点,思考和总结一下线程安全函数和可重入函数的区别和联系,它们已经在我的白板上待了很长时间了。
这里补充一下之前那个bug的具体原因:
首先场景是用户点击gui上的屏幕旋转按钮,这个下面会调用我这边的api,触发合成器进行旋转操作。
如果用户快速多次点击呢,则有大概率触发死锁。
在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)这个地方应该用的是tryLock的机制
首先,一个重要的结论是:可重入函数一定是线程安全的,但是线程安全函数不一定是可重入的
reentrant和thread safe的本质的区别就在于,reentrant函数要求即使在同一个线程中任意地进入两次以上,也能正确执行
我们一级一级的向着可重入函数出发,看看要成为可重入函数需要满足什么条件
1、首先线程不安全的函数,一定是不可重入的
这个不用多说了,线程不安全函数,指定是没有处理好对临界资源访问的互斥。
比如访问一个全局变量,或者使用系统调用,而且没有加锁,那么他就不是线程安全的。
为什么说不可重入呢,比如这个函数被中断了,中断服务中也用到了这个函数,那这里是不是还是会有问题。
一般来说,函数库的作者是有必要声明自己的api是否是线程安全的,以及是否有线程安全的版本
这里补充一下,可重入/不可重入函数的概念 若一个程序或子程序(或函数)可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,
则称其为可重入(reentrant)的。即当该子程序正在被执行是,执行线程可以再次进入并执行它,仍然获取符合预期的结果
2、线程不安全的函数可以通过加锁变成线程安全函数
但是即使加了锁,它也锁不了系统中断,或者信号处理函数,CPU的PC指针直接从mutex.lock()中跳出来,到了signal处理函数中,注意,这里尝试拿锁,结果锁在用户那里,于是就会死锁hung死在这里。因为中断等用户释放锁才能返回,用户等中断退出才能释放
这是因为一般的mutex也是不可重入的,即使使用可重入锁(递归锁),也可能带来别的问题,因为相当于,两个地方都觉得自己拿到锁了,然后就竞争了。所以,一般情况下,不用递归锁。理由如下:
递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好
一个著名的例子: printf线程安全,但是不可重入
3、可重入函数是线程安全的
这里有个限制条件,那就是满足以下条件的函数我们称之为可重入函数:
- 不能含有静态或全局非常量数据。
- 不能返回静态或全局非常量数据的地址
- 不能调用标准 I/O
- 调用的函数也必须是可重入的
- 没有动态分配或释放堆资源
因为有下面一个恶心人的例子,看起来可重入的函数,但是线程不安全,但是因为它用了全局非常量数据,我倾向于不认可这货是个可重入函数
作者:林嘉亮下面这个函数就很神奇:
int t;
void swap(int *x, int *y)
{
int s;
s = t;
t = *x;
*x = *y;
*y = t;
t = s;
}
如果同时在正常流程和信号处理程序中都调用了swap,不会造成错误的结果。因为无论信号在哪里发生并调用swap函数,最后都会把全局变量t进行恢复然后返回到正常流程。所以当正常流程执行完成后,全局变量t保持不变。这样说来,这个函数是可重入的。
但在多线程环境就有错误,首先线程A执行swap调用到 t = *x 语句,把全局变量t改变了。
然后线程B调用swap把全局变量t保存到变量s,然后切换到线程A把swap函数全部执行,最后切换到线程B执行完成,就会把全局变量t的值改变了(变成线程A中的*x)。这样说来,这个函数不是线程安全的。
链接:https://www.zhihu.com/question/21526405/answer/469884796
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。