美文网首页
linux用户空间 - 线程的可重入

linux用户空间 - 线程的可重入

作者: 404Not_Found | 来源:发表于2021-07-31 14:04 被阅读0次
    • 作者: 雪山肥鱼
    • 时间:20210727 13:27
    • 目的:从信号,多线程角度谈线程的可重入性
    # 典型场景分析
      ## 信号打断举例
    # 多线程编程的风险
      ## 多线程重入 - 小结
    # 不可重入举例与修正
    # 异步信号同步化 举例
    # 如何在信号处理函数中打印
    # 不可重入函数群
    

    典型场景分析:

    4根线程.png
    • 多CPU 下
      4根 线程在4个CPU 中, T1跑的同时 T2 也在跑。线程之间互相倒腾来 倒腾去,互相抢CPU的。
    • 单核
      由于是操作系统抢占调度或者时间片调度,T1跑的时候 T2 就进来的,T2跑的时候T3 就进来了。不会是同步的。要么基于优先级在抢,要么基于时间片在轮。这些线程都有可能访问同一块资源。

    线程之间会访问同一块资源,信号也有相同的属性。但是信号是线程内部的
    信号打断线程,也有可能会访问这根线程的同一资源。所以也会涉及安全问题。但信号是一根线程内部的,并非线程之间的。

    信号打断举例

    #include <stdio.h>
    struct tow_long {long a, b; } data;
    void singal_handler(int signum) {
      printf("%d\n, %d\n", data.a, data..b);
      alarm(1);
    }
    
    int main(void) {
      static struct two_long zero = {0, 0}, ones = {1,1};
      signal(SIGALRM, signal_handler);
      data = zeros;
      alarm(1);
      while(1);
        {data = zeros; data = ones;}
    }
    

    交替性给data赋值。 理论上每次都是 0, 0, 或者每次都是 1,1
    但是实际上是交替的:


    交替.png

    信号不是线程,是在打断这个线程的时候跑的,是属于这个线程的。

    有个想法就是加锁

    void signal_handler(int signum) {
       lock;
       printf();
       unlock
    }
    {pthread_mutex_lock data = zeros; unlock; lock data = ones; unlock}
    

    加锁其实从理论概念上就是错误的,因为锁是针对线程之间的,而信号处理函数是线程内部的。

    多线程编程的风险

    • 线程风险
      malloc
      free
      printf

    • 不访问全局资源的函数

    • 访问全局资源但是加上 mutex的函数
      在使用这些函数的时候,我们根本不用加锁,因为内部已经帮我们做好了。
      malloc 和 free 面对的场景其实很复杂,
      多线程申请内存的时候,面对的堆其实只有一个,这是有风险的。但malloc 和 free 不会出问题。

    • 信号风险

      • 不访问全局资源的函数
      • 访问全局资源但是保存和回复函数
      • 不能用mutex等
        但是 malloc 和 free 并不是信号安全的。因为信号进到线程后,也去malloc或者free这块堆,但是对被操作的堆来说,是无法用锁去保护的,因为锁是针对线程之间的。
    • 可重入的定义应从两方面出发

      • 信号安全定义
        对于linux来说 中断就是信号,中断的人依然可能调同一个函数, 从信号函数出来后,数据的统一性。强调的是异步安全。
      • 线程安全定义
        几个线程 操作同一个函数。

    线程不安全的,通常不可重入
    可重入的通常线程安全(极少数除外)

    各种举例见如下网站:
    https://deadbeef.me/2017/09/reentrant-threadsafe

    其实就是两种打断,不会造成冲突。
    信号打断:一次性执行完成,进去后执行完再出来。信号函数牛逼呀。
    线程打断:竞争关系,是互相打的。

    多线程可重入 - 小结

    可重入函数满足两条件:

    1. 函数是线程安全的
    2. 函数是可中断的,对于linux而言,异步的信号,执行了中断处理例程后,再回过头来继续执行函数,结果仍然正确。

    函数分类:


    图片.png

    举例 与 修正

    一个简单的大小写转换的代码:

    char * toupper(char * lower) {
      static char buffer[1000];
      lower -> buffer
      return buffer
    }
    T1: hello world
    T2: world hello
    

    如果这个时候,有两个线程操作这个函数。很明显会对static 数据 进行破坏。这个函数定是线程不安全的函数。

    可怕之处:99% 都是正确的。则一会正常一会不正常。

    #include <stdio.h>
    #include <pthread.h>
    #include <ctype.h>
    #include <sys/type.h>
    
    char * strtoupper(char * string) {
        static char buffer[1000];
        int index;
        
        for(index = 0; string[index]; index++)
            buffer[index] = touppfer(string[index]);
        buffer[index] = 0;
        
        return buffer;
    }
    
    
    void * thread_fun(void * param) {
        while(1) {
            unsleep(100);
            printf("%s\n", strtoupper((char*)param));
        }
    }
    
    int main(int argc ,char ** argv)
    {
        pthread_t tid1, tid2;
        int ret;
        
        printf("main pid:%d, tid:lu\n", getpid(), pthread_self());
        
        ret = pthread_create(&tid1, NULL, thread_fun, "hello world");
        if(ret == -1) {
            perror("can not create new thread");
            return 1;
        }
        
        ret = pthread_create(&tid2, NULL, thread_fun, "world hello") ;
        if(ret == -1) {
            perror("can not create new thread");
            return 1;
        }
        
        if(pthread_join(tid1, NULL) != 0) {
            perror("call pthread_join fail")
            return 1;
        }
        if(pthread_join(tid2, NULL) != 0) {
            perror("call pthread_join fail")
            return 1;
        }
        
        return 0;
    }
    

    查看真实输出:

    //搜索非HELLO 的 
    ./a.out |grep -v HELLO 
    

    结果明显会有冲突。可能会到处 HELLWO WELLO 等情况

    • 如果一个函数用到了全局或者静态变量,那么它不是线程安全的,也不是可重入的。
    • 改进:访问全局变量或者静态变量时,使用互斥量或者信号量等方式加锁,则时线程安全的。
      • 但这种改进方式,仅是线程之间安全的,仍然是不可重入的,因为通常加锁方式是针对不同线程访问,而对统一线程可能依旧出现问题。(信号打断)
    • 如果将函数中的全局或者静态变量去掉,改成函数参数等其他形式,则有可能使函数编程线程安全,又可重入。

    信号函数中不要调用 malloc printf free 这些函数。因为都是 信号层面不可重入的函数。否则堆会坏,打印会乱。

    修改如下:

    char * strtoupper(char * string, char * buffer) {
      int index;
      for(index = 0; string[index];index++)
          buffer[index] = toupper(string[index]);
      buffer[index] = 0;
      return buffer;
    }
    
    void * thread_fun(void * param) {
      while(1) {
        char buf[1000];
        uslepp(100);
        strtoupper((char*param, buff);
        printf("%s\n", buff);
      }
    }
    

    两个函数都调用的 thread_fun, 但是每个线程申请了自己的栈。所以是安全的

    异步信号同化 举例

    将可重入问题弱化成线程安全问题,因为信号是突然跳进来的东东,是异步的。即 把异步的东西 同步 化。

    增加 signal manager 线程来同步等信号。而不是让信号异步的跳进来。

    #include <signal.h>
    #include <errno.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    void sig_handler(int signum)
    {
        static int j = 0;
        static int k = 0;
        pthread_t  sig_ppid = pthread_self();
        // used to show which thread the signal is handled in.
    
        if (signum == SIGUSR1) {
            printf("thread %d, receive SIGUSR1 No. %d\n", sig_ppid, j);
            j++;
            //SIGRTMIN should not be considered constants from userland,
            //there is compile error when use switch case
        } else if (signum == SIGRTMIN) {
            printf("thread %d, receive SIGRTMIN No. %d\n", sig_ppid, k);
            k++;
        }
    }
    
    void* worker_thread()
    {
        pthread_t  ppid = pthread_self();
        pthread_detach(ppid);
        while (1) {
            printf("I'm thread %d, I'm alive\n", ppid);
            sleep(10);
        }
    }
    
    void* sigmgr_thread()
    {
        sigset_t   waitset, oset;
        siginfo_t  info;
        int        rc;
        pthread_t  ppid = pthread_self();
    
        pthread_detach(ppid);
    
        sigemptyset(&waitset);
        sigaddset(&waitset, SIGRTMIN);
        sigaddset(&waitset, SIGUSR1);
    
        while (1)  {
            rc = sigwaitinfo(&waitset, &info);
            if (rc != -1) {
                printf("sigwaitinfo() fetch the signal - %d\n", rc);
                sig_handler(info.si_signo);
            } else {
                printf("sigwaitinfo() returned err: %d; %s\n", errno, strerror(errno));
            }
        }
    }
    
    
    int main()
    {
        sigset_t bset, oset;
        int             i;
        pid_t           pid = getpid();
        pthread_t       ppid;
    
    
        // Block SIGRTMIN and SIGUSR1 which will be handled in
        //dedicated thread sigmgr_thread()
        // Newly created threads will inherit the pthread mask from its creator
        sigemptyset(&bset);
        sigaddset(&bset, SIGRTMIN);
        sigaddset(&bset, SIGUSR1);
        //A new thread inherits a copy of its creator's signal mask.
        if (pthread_sigmask(SIG_BLOCK, &bset, &oset) != 0)
            printf("!! Set pthread mask failed\n");
    
        // Create the dedicated thread sigmgr_thread() which will handle
        // SIGUSR1 and SIGRTMIN synchronously
        pthread_create(&ppid, NULL, sigmgr_thread, NULL);
    
        // Create 5 worker threads, which will inherit the thread mask of
        // the creator main thread
        for (i = 0; i < 5; i++) {
            pthread_create(&ppid, NULL, worker_thread, NULL);
        }
    
        // send out 50 SIGUSR1 and SIGRTMIN signals
        for (i = 0; i < 50; i++) {
            kill(pid, SIGUSR1);
            printf("main thread, send SIGUSR1 No. %d\n", i);
            kill(pid, SIGRTMIN);
            printf("main thread, send SIGRTMIN No. %d\n", i);
            sleep(10);
        }
        exit (0);
    }
    
    
    
    1. 所有线程屏蔽掉 SIGTMIN 和 SIGUSR1
    2. 创建一个线程,专门同步的等待这两个信号
      sigwaitinfo() 同步的等待信号的到来
    3. 创建子线程
    4. 主线程 kill 去给进程发信号
      此时某根线程在运行

    每隔10s输出一次结果:


    同步处理异步信号.png

    Tid: 385738496 专门处理信号的线程

    如何在信号处理函数中打印

    signal_handler() {
      //printf ->
      write
    }
    

    printf 直接 调用 write
    printf 是线程安全的,但并非信号安全。
    printf内部是有锁的,
    信号处理函数内部是不能有锁的
    因为自己线程如果拿了锁,又被信号中断,在printf里又拿了个锁,很容易造成死锁。 不仅仅是printf 自身打印出问题。程序可能会hang住。

    可以采用wirte, 直接用系统调用。

    不可重入函数:

    图片.png

    举例:

    char * asctime(const struct tm *tm);
    char * asctime_r(const struct tm * tm, char* buf);
    
    • 不可重入版本:转好的数据在哪里呢?一定是存在某个全局变量中
    • 可重入版本:传入一个参数,结果保存在这个buffer 里面即可。

    相关文章

      网友评论

          本文标题:linux用户空间 - 线程的可重入

          本文链接:https://www.haomeiwen.com/subject/hjqepltx.html