• 【Linux】线程控制


           🔥🔥 欢迎来到小林的博客!!
          🛰️博客主页:✈️林 子
          🛰️博客专栏:✈️ Linux
          🛰️社区 :✈️ 进步学堂
          🛰️欢迎关注:👍点赞🙌收藏✍️留言

    线程的异常终止

    进程内部是可以存在多个线程的。那么如果有一个线程出现了异常(产生了信号)。那么会发生什么后果呢?我们用下面这段代码来验证一下。

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      int id = *(int*)args;
      delete (int*)args;
      int count  = 0 ; 
      while(1)
      {
        printf("%d thread runing.... count = %d\n",id,count);
        //如果线程id为2, 且count计数到3,那么制造异常,产生信号 
        if(id == 2 && count == 3) 
        {
          int a = 10;
          a /= 0; 
        }
        sleep(1);
        count++;
      }
    }
    
    
    int main()
    {
      pthread_t tids[3]; 
      //创建三个线程
      for(int i = 0 ; i < 3; i  ++)
      {
        int* id = new int(i);
        pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
      }
      while(1)
      {
        printf("main thread runing...\n");
        sleep(2);
      }
      return 0 ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    该代码的逻辑就是创建3个线程,让第三个线程计数到3的时候 /0 产生信号。

    我们运行看看结果。

    在这里插入图片描述

    我们发现整个进程都崩溃了。所以可以得出结论:

    一个线程产生异常,那么所有线程都会崩溃。本质是因为所有线程共享信号处理函数,而信号来临时选择了默认处理方式,那么就会干掉整个进程。进程都被干掉了,那么内部的线程肯定也无一幸免。

    线程等待

    创建进程的时候,我们的父进程要等待子进程。否则子进程在结束时资源将无法释放,成为僵尸进程,造成内存泄漏。而线程这里也是一样的道理,如果主线程不等待子线程。子线程结束后,主线程还在运行,那么一样会造成资源的泄漏。所以等待子线程是很有必要的。

    线程等待函数:

    #include 
    int pthread_join(pthread_t thread, void **retval);
    第一个参数是要等待线程的tid 
    第二个参数是创建线程时传入的线程执行函数的返回值,在内核中会回调那个函数。而那个函数的返回值会返回给内核,再由内核输出到这个参数上。
    返回值:返回0为成功,非0为错误码。如果线程已分离还进行join,那么会返回-1
    • 1
    • 2
    • 3
    • 4
    • 5

    我们用以下代码来测试一下这个函数。

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      int id = *(int*)args;
      delete (int*)args;
      int count  = 0 ; 
      while(1)
      {
        printf("%d thread runing.... count = %d\n",id,count);
        sleep(1);
        if(count++ == 3) break; 
      }
      return (void*)id; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tids[3]; 
      //创建三个线程
      for(int i = 0 ; i < 3; i  ++)
      {
        int* id = new int(i);
        pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
      }
      
      //线程等待
      int* ids[3] = {0};
      pthread_join(tids[0],(void**)ids+0);    
      pthread_join(tids[1],(void**)ids+1);    
      pthread_join(tids[2],(void**)ids+2); 
    
      //打印三个线程的返回结果
      for(int i = 0 ; i < 3 ; i++)
        printf("thread %d : %ld\n",i,(long long)(ids[i]));
    
      return 0 ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    这段代码的逻辑就是 创建三个线程,每个线程执行三次循环,随后返回传入时的id。主线程等待三个线程,等待结束后打印三个线程的返回值。

    运行结果:

    在这里插入图片描述

    线程退出

    如果我们在线程内调用exit函数,那么整个进程都会退出。因为exit是进程退出函数,无论在哪调用,都会导致进程退出。进程退出,那么所有的线程也会被释放。

    所以线程退出,我们要用指定的退出函数。

    线程退出函数:

    #include 
    void pthread_exit(void *retval);
    参数是传入一个当前线程的返回值。
    
    • 1
    • 2
    • 3

    那么我们用代码来演示一下程序退出。

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      int id = *(int*)args;
      delete (int*)args;
      pthread_exit((void*)(id + 10));
      int count  = 0 ; 
      while(1)
      {
        printf("%d thread runing.... count = %d\n",id,count);
        sleep(1);
        if(count++ == 3) break; 
      }
      return (void*)id; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tids[3]; 
      //创建三个线程
      for(int i = 0 ; i < 3; i  ++)
      {
        int* id = new int(i);
        pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);
      }
      
      //线程等待
      int* ids[3] = {0};
      pthread_join(tids[0],(void**)ids+0);    
      pthread_join(tids[1],(void**)ids+1);    
      pthread_join(tids[2],(void**)ids+2); 
    
      //打印三个线程的返回结果
      for(int i = 0 ; i < 3 ; i++)
        printf("thread %d : %ld\n",i,(long long)(ids[i]));
    
      return 0 ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    代码逻辑很简单,一进线程就退出,传入的值为 传入的id+10。

    运行结果:

    在这里插入图片描述

    线程取消

    线程退出函数必须要在要退出的线程内部执行。但如果我想在主线程中让指定的线程退出。那么我们可以用线程取消的函数。

    线程取消函数:

    #include 
    int pthread_cancel(pthread_t thread);
    传入的是要取消的线程tid。
    返回值为0则成功,失败返回一个错误的非0数字。
    
    • 1
    • 2
    • 3
    • 4

    我们用一段代码来验证这个函数。

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      int count  = 0 ; 
      while(1)
      {
        printf("%s runing.... count = %d\n",(char*)args,count);
        sleep(1);
      }
      return nullptr; //返回自己的id
    }
    
    int main()
    {
      pthread_t tid; 
      pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
      
      sleep(3); 
      pthread_cancel(tid); //取消线程 
      sleep(3);
        
      return 0 ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    这段代码逻辑很简单,创建一个线程死循环执行。主线程三秒之后取消这个线程。再过三秒之后主线程结束(整个进程结束)。 在这个过程中我们实时监视。

    监视结果:

    在这里插入图片描述

    我们发现三面之前这个进程有2个线程,三秒之后就变成了一个线程。说明创建的线程被取消了,而三秒之后一个线程也没有了,说明主线程执行完毕,进程退出了。

    线程id

    我们可以用pthread_self()来返回线程id,并分别用16进制和10进制打印这个id。因为我提前知道了这个id会是个很大数(透剧怪登)。

    #include 
    pthread_t pthread_self(void);
    
    • 1
    • 2

    那么我们写段代码来打印一下线程的id。

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      int count  = 0 ; 
      pthread_t tid = pthread_self();
      while(1)
      {
        printf("%s runing.... count = %d ,thread id %ld , %lX \n",(char*)args,count,tid,tid);
        sleep(1);
      }
      return nullptr; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tid; 
      pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
      pthread_join(tid,nullptr) ; 
    
      return 0 ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这段代码的逻辑就是创建一个线程,然后通过pthread_self()函数获取当前线程的id,随后分别以10进制和16进制打印这个id。

    运行结果:

    在这里插入图片描述

    当10进制打印进程id时,我们会发现它是一个很大的数字,并且看不出什么,但是当我们以16进制打印时。我们惊奇的发现,这是不是很像一个地址?

    没错,线程id就是一个地址!

    那么就有疑问了,CPU不是LWP进行调度吗?为什么线程id不是LWP,却是一个很大的数字?我们要搞清楚一点,pthread库是一个动态库!!而LWP是系统内核管理线程的标识。线程id是pthread库管理线程的标识。

    在内核方面,用LWP来标识PCB进行调度,所以没有进程线程的区别。但是不要忘记了,线程是用pthread库创建出来的。那么创建出来要不要进行管理?这是必须的!!

    pthread如何管理线程

    首先我们都知道pthread是一个动态库,那么在程序执行的时候。会先把动态库加载进内存,随后根据页表把它映射到进程的共享空间内。

    在这里插入图片描述

    当pthread_create创建了一个线程时,那么就会在动态库中执行这个函数。随后就会创建一个struct pthread的结构体,线程局部存储以及线程栈。而这个结构体的地址(共享区中的虚拟地址)就是线程的id。这个结构体就是对这个线程的描述,存储着线程的信息,用来管理该线程。

    在这里插入图片描述

    虽然进程内部的线程都是共享进程地址空间的,那么就意味着进程地址空间中的栈(也叫主线程栈)可以被所有的线程访问。这是必然的,但访问归访问。线程本身的数据是不能存储在主线程栈中的。因为这样会导致存储十分混乱。也不能一个一个线程进行覆盖,这样会导致数据缺失。所以pthread动态库为每一个线程都提供了一个线程栈,这个线程栈是每个线程独有的(想跨线程访问也可以访问,但还是别这样做)。

    线程局部存储是什么呢?

    我们可以用__thread 来让一个全局变量变成线程私有的。

    先看一段代码:

    #include
    #include
    #include 
    
    int val = 0;
    
    void* ThreadRoutine(void* args)
    {
      while(1)
      {
        printf("new thread , val = %d , &val = %p \n",val,&val);
        sleep(1);
      }
      return nullptr; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tid; 
      pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
      while(1)
      {
        printf("main thread , val = %d, &val = %p\n",val,&val);
        val++;
        sleep(1);
      }
    
      pthread_join(tid,nullptr) ; 
      return 0 ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这段代码的逻辑就是创建一个线程不断打印全局变量,主线程也不断打印全局变量,但是主线程会在每打印一次自增一次全局变量。

    这段代码的结果是这样的:

    在这里插入图片描述

    没有意外,因为线程之间共享全局变量。所以打印的地址是一样的,值也是同步的。

    那么我们把int val换成__thread int val再来试试效果。

    #include
    #include
    #include 
    
    int val = 0;
    
    void* ThreadRoutine(void* args)
    {
      while(1)
      {
        printf("new thread , val = %d , &val = %p \n",val,&val);
        sleep(1);
      }
      return nullptr; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tid; 
      pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
      while(1)
      {
        printf("main thread , val = %d, &val = %p\n",val,&val);
        val++;
        sleep(1);
      }
    
      pthread_join(tid,nullptr) ; 
      return 0 ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    然后我们运行这段程序。

    运行结果:

    在这里插入图片描述

    我们发现两个val的值不同步了,地址也不一样了。

    原理很简单,本质就是在编译时,把val值的数据拷贝了一份。拷贝进了对应线程的线程局部存储中。

    结论:

    线程id本质是一个地址,这个地址在共享区中,与页表建立映射。最终映射到物理内存中对应的struct pthread结构体的地址起始处。在pthread_create创建了一个线程之后,pthread会在共享区创建一块内存。这个内存存储着线程的结构体,线程的局部存储以及线程栈。 而通过线程id可以找到线程结构体的起始位置。

    线程分离

    如果我们的主线程创建了线程之后却不想管它。那么我们可以用pthread_detach来让线程分离。 用pthread_detach来分离线程,那么被分离的线程在结束后就会自动销毁。如果不detach也不join,那么线程的一些资源就无法被释放,此时的线程就会陷入与僵尸进程相似的状态。

    线程分离函数:

    int pthread_detach(pthread_t thread);
    参数是传入要分离线程的tid。
    返回值:0为成功,非0则分离失败。
    
    • 1
    • 2
    • 3

    pthread_detach代码:

    #include
    #include
    #include 
    
    void* ThreadRoutine(void* args)
    {
      pthread_detach(pthread_self());
      int count = 5;
      while(count--)
      {
        printf("new thread , count = %d\n",count);
        sleep(1);
      }
      return nullptr; //返回自己的id
    }
    
    
    int main()
    {
      pthread_t tid; 
      pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");
      while(1)
      {
        printf("main thread runing.... \n");
        sleep(1);
      }
    
      return 0 ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
  • 相关阅读:
    Python - 小玩意 - 请求网络地址获取网页链接
    R语言使用data.table包的fread函数读取(加载)csv数据为data.table格式、使用anyNA函数判断data.table中是否存在缺失值
    Linux centos7.6 安装elasticsearch8.x (es8) 教程
    螺旋矩阵问题C代码
    在Linux上安装RStudio工具并实现本地远程访问【内网穿透】
    Linux 使用gcc编译一个helloworld程序
    H5游戏源码分享-网页版2048小游戏
    [java刷算法]牛客—剑指offer链表复习、手写简易正则匹配
    [C++]——带你学习类和对象
    【FreeRTOS】【STM32】06.1 FreeRTOS的使用1(对06的补充)
  • 原文地址:https://blog.csdn.net/Lin5200000/article/details/133691159