可重入函数

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

 main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完 第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于 是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节 点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从 main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二 步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真 正插入链表中了。    

insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次 进入该函 数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样 的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入 (Reentrant) 函数。

可重入,并不一定要是多线程的。可重入只关注一个结果可再现性。在APUE中,可函数可重入的概念最先是在讲signal的handler的时候提出的。此时进程(线程)正在执行函数fun(),在函数fun()还未执行完的时候,突然进程接收到一个信号sig, 此时,需要暂停执行fun(),要转而执行sig信号的处理函数sig_handler(),那么,如果在sig_handler()中,也恰好调用了函数fun().信号的处理是以软终端的形式进行的,那么,当sig_handler()执行完返回之后,CPU会继续从fun()被打断的地方往下执行。这里讲的比较特殊,最好的情况是,进程中调用了fun(),函数,信号处理函数sig_handle()中也调用了fun()。如果fun()函数是可重入的,那么,多次调用fun()函数就具有可再现性。从而,两次调用fun()的结果是正确的预期结果。非可重入函数,则恰好相反。

简而言之,可重入函数,描述的是函数被多次调用但是结果具有可再现性。

如果fun(),中,使用了static变量、返回全局变量、调用非可重入函数等等,带有全局性的操作,都将会导致2次以上调用fun()的结果的不可再现性(当然,有些时候使用了static、全局变量等等,不一定导致调用结果不可再现性)。只要使调用结果具有可再现性,那么该函数就是可重入的。

为了保证函数是可重入的,需要做到一下几点:

1,不在函数内部使用静态或者全局数据

2,不返回静态或者全局数据,所有的数据都由函数调用者提供

3,使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

4, 如果必须访问全局数据,使用互斥锁来保护

5,不调用不可重入函数

重入函数的分类

可重入函数

显式可重入函数:如果所有函数的参数都是传值传递的(没有指针),并且所有的数据引用都是本地的自动栈变量(也就是说没有引用静态或全局变量),那么函数就是显示可重入的,也就是说不管如何调用,我们都可断言它是可重入的。

隐式可重入函数:可重入函数中的一些参数是引用传递(使用了指针),也就是说,在调用线程小心地传递指向非共享数据的指针时,它才是可重入的。例如rand_r就是隐式可重入的。
我们使用可重入(reentrant)来包括显式可重入函数和隐式可重入函数。然而,可重入性有时是调用者和被调用者共有的属性,并不只是被调用者单独的属性。

线程安全

APUE中的描述:If a function can be safely called by multiple threads at the same time, we say that the function is thread-safe

如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们说这个函数是线程安全的。所谓安全,一切可能导致结果不正确的因素都是不安全的调用。

一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的(thread-unsafe)。我们定义四类(有相交的)线程不安全函数。

第1类:不保护共享变量的函数

将这类线程不安全函数变为线程安全的,相对比较容易:利用像P和V操作这样的同步操作来保护共享变量。这个方法的优点是在调用程序中不需要做任何修改,缺点是同步操作将减慢程序的执行时间。

第2类:保持跨越多个调用的状态函数

一个伪随机数生成器是这类不安全函数的简单例子。

unsigned int next = 1; int rand(void){
     next = next * 1103515245 + 12345;
     return (unsigned int) (next / 65536) % 32768;} void srand(unsigned int seed){
      next = seed;}

rand函数是线程不安全的,因为当前调用的结果依赖于前次调用的中间结果。当我们调用srand为rand设置了一个种子后,我们反复从一个单线程中调用rand,我们能够预期一个可重复的随机数字序列。但是,如果有多个线程同时调用rand函数,这样的假设就不成立了。

使得rand函数变为线程安全的唯一方式是重写它,使得它不再使用任何静态数据,取而代之地依靠调用者在参数中传递状态信息。这样的缺点是,程序员现在要被迫改变调用程序的代码。

第3类:返回指向静态变量指针的函数

某些函数(如gethostbyname)将计算结果放在静态结构中,并返回一个指向这个结构的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程使用的结果会被另一个线程悄悄地覆盖了。

有两种方法来处理这类线程不安全函数。一种是选择重写函数,使得调用者传递存放结果的结构地址。这就消除了所有共享数据,但是它要求程序员还要改写调用者的代码。

如果线程不安全函数是难以修改或不可修改的(例如,它是从一个库中链接过来的),那么另外一种选择就是使用lock-and-copy(加锁-拷贝)技术。这个概念将线程不安全函数与互斥锁联系起来。在每个调用位置,对互斥锁加锁,调用函数不安全函数,动态地为结果非配存储器,拷贝函数返回的结果到这个存储器位置,然后对互斥锁解锁。一个吸引人的变化是定义了一个线程安全的封装(wrapper)函数,它执行lock-and-copy,然后调用这个封转函数来取代所有线程不安全的函数。例如下面的gethostbyname的线程安全函数。

struct hostent* gethostbyname_ts(char* host){
    struct hostent* shared, * unsharedp;
    unsharedp = Malloc(sizeof(struct hostent));
    P(&mutex)
    shared = gethostbyname(hostname);
    *unsharedp = * shared;
    V(&mutex);
    return unsharedp;}

第4类:调用线程不安全函数的函数

如果函数f调用线程不安全函数g,那么f就是线程不安全的吗?不一定。如果g是类2类函数,即依赖于跨越多次调用的状态,那么f也是不安全的,而且除了重写g以外,没有什么办法。然而如果g是第1类或者第3类函数,那么只要用互斥锁保护调用位置和任何得到的共享数据,f可能仍然是线程安全的。比如上面的gethostbyname_ts。

线程安全,是针对多线程而言的。那么和可重入联系起来,我们可以断定,可重入函数必定是线程安全的,但是线程安全的,不一定是可重入的。不可重入函数,函数调用结果不具有可再现性,可以通过互斥锁等机制,使之能安全的同时被多个线程调用,那么,这个不可重入函数就是转换成了线程安全。

线程安全,描述的是函数能同时被多个线程安全的调用,并不要求调用函数的结果具有可再现性。也就是说,多个线程同时调用该函数,允许出现互相影响的情况,这种情况的出现需要某些机制比如互斥锁来支持,使之安全。

联系:可重入函数一定是线程安全的,但线程安全不一定是可重入的。