函数的可重入与线程安全有什么关系

描述

在嵌入式裸机时代,也就是无OS时代,我们在裸机环境下编写C语言程序非常简单,实现一个函数,然后将函数接口API提供给其它模块调用就可以了。比如下面的函数,我们实现一个sum函数,用来求两个数的和:

函数

但是在一个运行OS的多任务环境中,我们在编写sum函数时就要注意一些细节了:我们编写的sum函数可能会被多个任务调用,而且可能在sum函数执行的过程被打断,接着在另一个任务中再次调用sum函数。而在上面的sum函数实现中,我们定义了一个静态变量sum用来保存两个数相加的临时结果,静态变量是保存到数据段中的,大家可以想一想,在一个任务A中正在执行sum(1,2)函数中的第4行,此时任务被打断挂起,接着运行任务B,在任务B中接着执行sum(10,20)函数,执行结束后接着运行任务A,A获得CPU控制权后继续运行sum(1,2)的第5行,此时sum(1,2)的返回结果就变成了30,而不是正确结果3。

在一个多任务环境中,如果一个函数可以重复并发调用,而且多次调用并不会影响函数的运行结果,那么这个函数是可重入的,我们称这个函数为:可重入函数。在上面的sum函数实现中,当其被多次并发调用时,函数的运行结果并不确定,我们称其为不可重入函数。

我们如何去判定一个函数是可重入的,还是不可重入的呢?很简单,当一个函数满足下面任一条件,那么这个函数就是不可重入函数。

  • 函数内部使用了全局变量
  • 函数内部使用了静态局部变量
  • 函数返回值为全局变量或静态变量
  • 函数内部使用了malloc/free函数
  • 函数北部使用了标准I/O函数
  • 函数内部调用其它不可重入函数

不可重入函数在一个多任务环境中不能被多次并发调用,如果一个函数可能被多次调用,那么我们设计这个函数时尽量要将其设计为可重入函数。

  • 不使用/返回静态变量、全局变量
  • 不使用标准I/O函数
  • 不使用malloc/free函数
  • 不调用不可重入函数

在函数设计时,只要注意上面的原则,那么我们就可以将一个函数设计为可重入函数,可重入函数在多任务环境下可以被多次并发调用,是线程安全的,程序员可以放心大胆地调用。

理想很丰满,现实很骨干。我们在编程中如果说不用malloc/free、全局变量,那是不现实的。只要我们使用了这些全局变量,静态变量,那么函数就变成不可重入了,在多任务环境下使用这个函数就变得线程不安全了,那怎么办呢?

方法还是有的,一个函数之所以变得不可重入,就是因为函数内有一些资源是全局共享的,在多任务环境下多次并发调用该函数时可能会破坏掉这些共享的全局资源。我们如果把这些资源在访问的时候保护起来,不让其它任务访问(即互斥访问),即同一时刻只允许一个进程访问就安全了。这些被保护的资源我们称为临界资源,访问这些临界资源的代码段,我们称之为临界区。临界区的访问方式为互斥访问,即同一时刻只允许一个进程访问。

临界区的实现方式有很多种,不同的操作系统可能会提供不同的实现方式。我们可以通过下面的操作原语来实现一个临界区:

  • EnterCriticalSection()
  • LeaveCriticalSection()

不同的操作系统,具体的实现手段可能不一样,常见的方法有:关中断;实现互斥访问,比如通过信号量、互斥量、自旋锁等实现,甚至原子操作等。比如在uc/os操作系统中,我们使用关中断的方式来实现临界区,确保函数的线程安全。

函数

而在linux/windows操作系统中,我们通常使用锁机制来实现临界区:

函数

在一个不可重入函数中,通过临界区来实现共享全局资源的互斥访问,那么在多任务环境下调用这个函数也就变得安全了,也就是说这个不可重入函数是线程安全的。

通过上面的分析,我们可以得出下面的结论:一个函数如果是可重入函数,那么这个函数是线程安全的,其它进程线程都可以对这个函数并发访问,并不会影响函数的运行结果。如果一个函数是不可重入函数,我们通过临界区设计对共享全局资源进行互斥访问,也可以让这个函数变得线程安全,其它进程线程也可以放心调用。由此,我们得出线程安全与可重入之间的关系如下:

函数

也就是说,一个可重入函数肯定是线程安全的,而线程安全函数并不一定是可重入函数,不可重入函数也有可能是线程安全的,比如我们常见的malloc函数,就是不可重入函数,但是是线程安全的,为什么呢?

通过《C语言嵌入式Linux高级编程》课程学习,我们已经知道,对于我们使用malloc/free申请释放的内存,glibc在用户空间实现了一个内存管理器,将各个大小的内存块链成多个全局链表进行管理。

函数

当我们使用malloc/free申请释放内存时,如果申请/释放的内存块大小符合规定,一般都是直接对这些全局链表进行操作、避免多次系统调用进入内核态,减少系统开销。因为malloc/free函数对全局链表进行了操作,所以malloc/free是不可重入函数。在访问这些全局链表时,我们需要通过锁机制加以保护,每次malloc/free操作全局链表时,其它地方就被互斥访问了,只有当malloc/free操作全局链表完成退出,其它地方的malloc/free才能对这个全局链表进行访问。

通过上面的分析,我们可以看到:malloc/free虽然是不可重入函数,但是通过加锁对共享全局资源的互斥访问,也就变得线程安全了,在多任务环境下,每个进程都可以放心大胆地调用它:因为malloc虽然是不可重入函数,但它是线程安全的。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分