美文网首页
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 里面即可。

相关文章

  • Java中的各种锁

    一个线程中的多个流程能不能获取同一把锁:可重入锁和非可重入锁 可重入锁 可重入性:表明了锁的分配机制,是基于线程的...

  • 锁 - 可重入 vs 不可重入

    可重入锁 在多线程编程和信号处理过程中,经常会遇到可重入(reentrance)和线程安全(thread-safe...

  • 多线程篇五(ReentrantLock源码解析 公平锁与非公平锁

    前言ReentrantLock类是实现了Lock接口的可重入锁,所谓可重入锁指的是以线程为单位,当一个线程获取对象...

  • Qt:可重入和线程安全

    线程安全函数也是可重入函数,但可重入不一定是线程安全。 A thread-safe function is alw...

  • 操作系统相关

    1、什么是可重入函数 可重入函数是指能够被多个线程“同时”调用的函数(线程安全),并且能够保证结果的正确性的函数。...

  • Qt 可重入和线程安全

    可重入和线程安全 本文翻译自Qt官网:重入和线程安全[https://doc.qt.io/qt-5/threads...

  • zookeeper实现分布式锁

    闲来无事,用Java的zk客户端Curator实现了一个简易的分布式可重入锁,这里的可重入指的是线程可重入,如果有...

  • 实操Redission 分布式锁和同步器

    1. 可重入锁(Reentrant Lock) 概念:所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这...

  • ReentrantLock & AQS

    Introduction:可重入锁,靠state参数来记录重入次数,靠waitStatus参数来监控线程状态,靠双...

  • golang可重入锁的实现

    如何实现可重入锁?实现一个可重入锁需要这两点:1.记住持有锁的线程2.统计重入的次数 转自golangroadma...

网友评论

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

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