线程安全函数和可重入函数 辨析

偶然想起之前项目里面遇到的一个操作合成结果翻转导致的画面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
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

posted on 2021-12-18 21:58  疾速瓜牛  阅读(222)  评论(0编辑  收藏  举报

导航