zoukankan      html  css  js  c++  java
  • 可重入函数与线程安全

    线程安全一个函数被称为线程安全的(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。

    可重入函数

    可重入函数:可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
    可重入函数通常要比不可重入的线程安全函数效率高一些,因为它们不需要同步操作。更进一步说,将第2类线程不安全函数转化为线程安全函数的唯一方法就是重写它,使之可重入。

    下面为rand函数的一个可重入版本

    int rand_r(unsigned int* nextp)
    {
         *nextp = *nextp * 1103515245 + 12345;
          return (unsigned int) (*nextp / 65536) % 32768;
    }

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

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


    extern:

    不可重入函数不可以在它还没有返回就再次被调用;该函数在被调用还没有结束之前,再次被调用,从而可能产生错误。 但是,可重入函数不存在这样的问题。

    不可重入函数在实现时通常使用了全局的资源(eg. 全局变量),在多线程的环境下,如果没有很好的处理数据保护互斥访问,就可能会发生错误。

    常见的不可重入函数有:

    printf  --- 引用全局变量stdout;

    malloc  --- 全局内存分配表,分配堆栈空间;

    free  --- 全局内存分配表,释放堆栈空间;

    以prinf为例,中断在任何时候都可能发生,比如发生在prinft执行过程中,假若又发生了中断嵌套,而此时stdout资源被占用,所以第二个中断的printf等待第一个中断的stdout资源释放,而第一个中断等待第二个中断返回,造成了死锁,这样printf就发生了重入,这种情况是不允许的。

    在unix里面,通常会有加上_r的后缀同名可重入函数版本。如果实在没有,不妨在可预见的发生错误的地方尝试加上保护锁同步机制等。



    摘自:http://blog.xiaoheshang.info/?cat=11

  • 相关阅读:
    搜索文件/目录的shell脚本
    git的编译安装
    linux命令行直接执行MySQL/MariaDB语句查询
    MySQL重置root密码
    图解TCP/IP三次握手
    使用pull命令从Docker Hub仓库中下载镜像到本地
    BZOJ1051 [HAOI2006]受欢迎的牛(Tarjan缩点)
    BZOJ1026 [SCOI2009]windy数(数位DP)
    CERC2017 H Hidden Hierarchy(树+模拟)
    2018icpc徐州网络赛-H Ryuji doesn't want to study(线段树)
  • 原文地址:https://www.cnblogs.com/Zoran-/p/5819291.html
Copyright © 2011-2022 走看看