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

可重入函数
node

wKiom1eF3qPDXs6AAAB5lzVuCiE996.png

 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、全局变量等等,不必定致使调用结果不可再现性)。只要使调用结果具备可再现性,那么该函数就是可重入的。app

为了保证函数是可重入的,须要作到一下几点:ide

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

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

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。

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

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

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

相关文章
相关标签/搜索