转自可重入
若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。一个子程序的重入,可能由于自身原因,如执行了jmp或者call,类似于子程序的递归调用;或者由于硬件中断,UNIX系统的signal的处理,即子程序被中断处理程序或者signal处理程序调用。重入的子程序,按照后进先出线性序依次执行。
若一个函数是可重入的,则该函数:
- 不能含有静态(全局)非常量数据。
- 不能返回静态(全局)非常量数据的地址。
- 只能处理由调用者提供的数据。
- 不能依赖于单实例模式资源的锁。
- 不能调用(call)不可重入的函数(有呼叫(call)到的函数需满足前述条件)。
多“用户/对象/进程优先级”以及多进程,一般会使得对可重入代码的控制变得复杂。同时,IO代码通常不是可重入的,因为他们依赖于像磁盘这样共享的、单独的(类似编程中的静态(Static)、全域(Global))资源。
可重入性是函数编程语言的关键特性之一。
例子
在以下的C语言代码中,函数f和函数g都不是可重入的。
int g_var = 1; int f() { g_var = g_var + 2; return g_var; } int g() { return f() + 2; }
以上代码中,f使用了全局变量 g_var,所以,如果两个线程同时执行它并访问g_var,则返回的结果取决于执行的时间。因此,f不可重入。而g调用了f,所以它也不可重入。
稍作修改后,两个函数都是可重入的:
int f(int i) { return i + 2; } int g(int i) { return f(i) + 2; }
与线程安全的关系
可重入与线程安全两个概念都关系到函数处理资源的方式。但是,他们有一定的区别。可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。
- 大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。
- 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。
线程安全与可重入性是两个不同性质的概念。可重入是在单线程操作系统背景下,重入的函数或者子程序,按照后进先出的线性序依次执行完毕。多线程执行的函数或子程序,各个线程的执行时机是由操作系统调度,不可预期的,但是该函数的每个执行线程都会不时的获得CPU的时间片,不断向前推进执行进度。可重入函数未必是线程安全的;线程安全函数未必是可重入的。例如,一个函数打开某个文件并读入数据。这个函数是可重入的,因为它的多个实例同时执行不会造成冲突;但它不是线程安全的,因为在它读入文件时可能有别的线程正在修改该文件,为了线程安全必须对文件加“同步锁”。另一个例子,函数在它的函数体内部访问共享资源使用了加锁、解锁操作,所以它是线程安全的,但是却不可重入。因为若该函数一个实例运行到已经执行加锁但未执行解锁时被停下来,系统又启动该函数的另外一个实例,则新的实例在加锁处将转入等待。如果该函数是一个中断处理服务,在中断处理时又发生新的中断将导致资源死锁。
下述例子,是线程安全的,但不是可重入的。
int function() { mutex_lock(); ... function body ... mutex_unlock(); }
多线程执行时,获得了互斥锁的线程总能获得CPU时间片,向前推进执行进度,最终解开互斥锁,使得别的线程也能获得互斥锁进入临界区。但是,如果在单线程背景下第一次执行该函数时已经获得互斥锁进入临界区,这时该函数被重入执行,这将在重新申请互斥锁时被饿死(starvation),因为获得了互斥锁的该函数的第一次执行将永远没有机会再获得CPU时间片。