• 线程控制之重入


    有了信号处理程序和线程,多个控制线程在同一时间可能潜在地调用同一个函数。

    如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。在Single UNIX Specification中定义的所有函数,除了表12-5中列出的函数以外,其他函数都保证是线程安全的。另外,ctermid和tmpnam函数在参数传入空指针时并不能保证是线程安全的。类似地,wcrtomb和wcsrtombs函数如果参数mbstate_t传入的是空指针的话,也不能保证它们是线程安全的。

    表12-5 POSIX.1中不能保证线程安全的函数(此表摘自第三版)

    未命名

    支持线程安全函数的操作系统实现会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。应用程序可以在sysconf函数中传入_SC_THREAD_SAFE_FUNCTIONS参数,以在运行时检查是否支持线程安全函数。所有遵循XSI的实现要求必须支持线程安全函数。

    操作系统实现支持线程安全函数这一特性时,对POSIX.1中的一些非线程安全函数,它会提供可替代的线程安全版本,表12-6列出了这些函数的线程安全版本。很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全的。

    表12-6 替代的线程安全函数(此表摘自第三版)

    未命名

    表12-6中列出的函数的命名方式与他们的非线程安全版本的名字相似,只不过在名字最后加了_r,以表明这个版本是可重入的。

    如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步-信号安全的。

    除了表12-6中列出的函数,POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取与给定FILE对象关联的锁。这个锁是递归的,当占有这把锁的时候,还可以再次获取该锁,这并不会导致死锁。虽然这种锁的具体实现并无规定,但要求所有操作FILE对象的标准I/O例程表现得就像它们内部调用了flockfile和funlockfile一样。

    #include <stdio.h>
    
    int ftrylockfile(FILE *fp);
    返回值:若成功则返回0,否则返回非0值
    
    void flockfile(FILE *fp);
    void funlockfile(FILE *fp);

    虽然标准的I/O例程从它们各自的内部数据结构这一角度出发,可能是以线程安全的方式实现的,但有时把锁开放给应用程序仍然是非常有用的。这允许应用程序把多个对标准I/O函数的调用组合成原子序列。当然,在处理多个FILE对象时,需要注意可能出现的死锁,并且需要对所有的锁仔细地排序。

    如果标准I/O例程都获取它们各自的锁,那么在做一次一个字符的I/O操作时性能就会出现严重的下降。在这种情况下,需要对每一个字符的读或写操作进行获取锁和释放锁的动作。为了避免这种开销,出现了不加锁版本的基于字符的标准I/O例程。

    #include <stdio.h>
    
    int getchar_unlocked(void);
    int getc_unlocked(FILE *fp);
    两者的返回值都是:若成功则返回一个字符,若已到达文件结尾或出错则返回EOF
    
    int putchar_unlocked(int c);
    int putc_unlocked(int c, FILE *fp);
    两者的返回值都是:若成功则返回c,若出错则返回EOF

    除非被flockfile(或ftrylockfile)和funlockfile的调用包围,否则尽量不要调用这四个函数,因为它们会导致不可预期的结果(即由多个控制线程非同步地访问数据所引起的种种问题)。

    一旦对FILE对象进行加锁,就可以在释放锁之前对这些函数进行多次调用。这样就可以在多次的数据读写上分摊总的加解锁的开销。

     实例

    程序清单12-3显示了getenv(http://www.cnblogs.com/nufangrensheng/p/3508319.html)一个可能的实现。因为所有调用getenv的线程返回的字符串都存放在同一个静态缓冲区中,所以这个版本不是可重入的。如果两个线程同时调用这个函数,就会看到不一致的结果。

    程序清单12-3 getenv的非可重入版本

    #include <limits.h>
    #include <signal.h>
    
    static char envbuf[ARG_MAX];
    
    extern char **environ;
    
    char *
    getenv(const char *name);
    {
        int i, len;
        
        len = strlen(name);
        for(i = 0; environ[i] != NULL; i++)
        {
            if((strncmp(name, environ[i], len) == 0) &&
               (environ[i][len] == '='))
            {
                strcpy(envbuf, &environ[i][len+1]);
                return(envbuf);
            }
        }
        return(NULL);
    }

    程序清单12-4给出了getenv的可重入版本,这个版本命名为getenv_r。它使用pthread_once函数来确保每个进程只调用一次thread_init函数。

     程序清单12-4 getenv的可重入(线程安全)版本

    #include <string.h>
    #include <errno.h>
    #include <pthread.h>
    #include <stdlib.h>
    
    extern char **environ;
    
    pthread_mutex_t env_mutex;
    static pthread_once_t init_done = PTHREAD_ONCE_INIT;
    
    static void
    thread_init(void)
    {
        pthread_mutexattr_t attr;
        
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&env_mutex, &attr);
        pthread_mutexattr_destroy(&attr);
    }
    
    int
    getenv_r(const char *name, char *buf, int buflen)
    {
        int i, len, olen;
        
        pthread_once(&init_done, thread_init);
        len = strlen(name);
        pthread_mutex_lock(&env_mutex);
        for(i = 0; environ[i][len] != NULL; i++)
        {
            if((strncmp(name, environ[i], len) == 0) &&
              (environ[i][len] == '='))
            {
                olen = strlen(&environ[i][len+1]);
                if(olen >= buflen)
                {
                    pthread_mutex_unlock(&env_mutex);
                    return(ENOSPC);
                }
                strcpy(buf, &environ[i][len+1]);
                pthread_mutex_unlock(&env_mutex);
                return(0);
            }
        }
        pthread_mutex_unlock(&env_mutex);
        return(ENOENT);
    
    }

    要使getenv_r可重入,需要改变接口,调用者必须自己提供缓冲区,这样每个线程可以使用各自不同的缓冲区从而避免其他线程的干扰。但是注意这还不足以使getenv_r成为线程安全的,要使getenv_r成为线程安全的,需要在搜索请求的字符串时保护环境不被修改。我们可以使用互斥量,通过getenv_r和putenv函数对环境列表的访问进行序列化。

    可以使用读写锁,从而允许对getenv_r的多次并发访问,但并发性的增强可能并不会在很大程度上改善程序的性能。这里面有两个原因:首先,环境列表通常不会很长,所以扫描列表时并不需要长时间地占有互斥量;其次,对getenv和putenv的调用不是频繁发生的,所以改善它们的性能并不会对程序的整体性能产生很大的影响。

    即使把getenv_r变成线程安全的,也并不意味着它对信号处理程序是可重入的。如果使用的是非递归的互斥量,当线程从信号处理程序中调用getenv_r时,就有可能出现死锁。如果信号处理程序在线程执行getenv_r时中断了该线程,由于这时已经占有加锁的env_mutex,这样其他线程试图对这个互斥量的加锁就会被阻塞,最终导致线程进入死锁状态。所以,必须使用递归互斥量阻止其他线程改变当前正在查看的数据结构,同时还要阻止来自信号处理程序的死锁。问题是pthread函数并不保证是异步信号安全的,所以不能把pthread函数用于其他函数,让该函数成为异步信号安全的。

    本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/

  • 相关阅读:
    hadoop集群管理之 SecondaryNameNode和NameNode
    无法fsck问题解决
    处世
    [THUSC2016]成绩单
    Python安装官方whl包、tar.gz包、zip包
    poj1159 Palindrome 动态规划
    hoj1249 Optimal Array Multiplication Sequence 矩阵链乘
    hoj分类(转)
    hoj 2012 winter training Graph Day1 106 miles to Chicago图论最短路dijkstra算法
    poj1050 To the Max
  • 原文地址:https://www.cnblogs.com/nufangrensheng/p/3537972.html
Copyright © 2020-2023  润新知