• Linux 线程概念&线程控制


                                                         

           这些天家里格外的热,同时自己还偷懒了几天。博客一直没来得及更新,一想到为了冲刺实习生,自己还是不能堕落。希望和老铁们一起过一个充实的暑假~今天我们来学习Linux多线程~它和进程有什么区别呢?

    目录

    线程概念

    Windows下线程的概念

    Linux下的线程

    总结

    Linux 线程vs进程

    Linux线程控制

    线程创建 

    pthread_create

    pthread_self

    查看线程方法

    线程等待

    pthread_join

    注意 

    线程终止

    pthread_exit

    pthread_exit和exit

    pthread_cancel

    注意

    线程分离

    pthread_detach 

    线程id和LWP

    如何理解线程tid?

    用户级线程和内核轻量级线程的关系 


    线程概念

    Windows下线程的概念

    一般教材线程的概念:线程是进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化。 

    但是,今天我们讲的线程和上面的不一样,Linux下设计的线程是复用了进程PCB,具体是怎么设计的呢?我们今天来重点学习一下:

    Linux下的线程

           实际上,Linux下的并没有真正意义上的线程,而是对进程PCB的复用。每创建一个线程时,就在当前进程内创建一个PCB,和主线程指向同一个地址空间。但是在CPU看来,它只关心该进程的PCB,对于进程内部的"线程",CPU就不去看,线程是被当前进程的主线程进行管理。 

    总结

    CPU此时看到的PCB <= 之前讲的PCB的概念

    一个PCB就是一个需要被调度的执行流!

    Linux中没有专门为线程设计TCB,而是用进程的PCB来模拟线程的,这样做有什么好处?

    不用维护复杂的进程和线程之间的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法。OS只需要聚焦在进程的资源分配上就可以了。

    Linux 线程vs进程

    区别一

    所有的轻量级进程(可能是"线程")都是在进程内部执行(地址空间:表示进程所能看到的大部分资源)

    进程:具有独立性,大部分资源是私有的,可以有部分资源共享(管道、ipc资源)

    线程:大部分资源是共享的,可以有部分资源是私有的(栈,上下文)

    区别二

    Linux线程控制

    Linux下的线程是用进程模拟的,所以Linux下不会给我们提供直接操作线程的接口,而是给我们提供的,在同一个地址空间内创建PCB的方法,分配资源给指定的接口,这样做对用户使用很不友好,所以就有人实现第三方库来供我们使用!

    线程创建 

    pthread_create

    pthread_t* thread:输出型参数,返回创建线程的id

    const pthread_attr_t* arr:线程属性,我们在这里设置成NULL就可以

    void *(*start_routine) (void *):创建的线程执行方法,函数指针

    void* arg:给回调函数传的参数

    pthread_self

    返回当前线程的id(注意:和下面的LWP不一样!)

    代码测试:

    1. void* thread_run(void* args)
    2. {
    3. const char* id = (const char*)args;
    4. while(1)
    5. {
    6. printf("我是%s线程: %d\n", id, getpid());
    7. sleep(1);
    8. }
    9. }
    10. int main()
    11. {
    12. pthread_t tid;
    13. pthread_create(&tid, NULL, thread_run, (void*)"thread 1");
    14. while(1)
    15. {
    16. printf("我是main主线程: %d\n", getpid());
    17. sleep(1);
    18. }
    19. return 0;
    20. }

    运行结果:

    查看线程方法

    [cyq@VM-0-7-centos 线程控制]$ ps -aL
    

     

     这时候我们就发现了两个线程,它们的PID都是一样的,说明他们属于同一个进程!LWP是线程ID。

    线程等待

    为什么要有线程等待?

    一般而言,线程也是需要被等待的,如果不等待,可能导致类似于"僵尸进程"问题。

    pthread_join

    pthread_t thread:要等待线程的id

    void** retval:输出型参数,用来获取新线程退出的时候,线程函数的返回值。 

    返回值:等待成功返回0,等待失败,返回错误码。

    举个栗子:

    1. void* thread_run(void* args)
    2. {
    3. int id = *(int*)args;
    4. while(1)
    5. {
    6. printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
    7. sleep(5);
    8. break;
    9. }
    10. return (void*)123;
    11. }
    12. #define NUM 1
    13. int main()
    14. {
    15. pthread_t tid[NUM];
    16. int i = 0;
    17. for(i = 0; i < NUM; i++)
    18. {
    19. pthread_create(tid+i, NULL, thread_run, (void*)&i);
    20. sleep(1);
    21. }
    22. void* status = NULL;
    23. for(i = 0; i < NUM; i++)
    24. {
    25. pthread_join(tid[i], &status); //阻塞式等待
    26. }
    27. printf("ret: %d\n", (int)status);
    28. sleep(3);
    29. return 0;
    30. }

    运行结果:

    这时候我们发现等待可以获取创建线程的返回结果。

    注意 

    pthread_join:是一种阻塞式等待,必须是阻塞式等待,直到新线程退出才能拿到对应的返回值。

    printf("ret: %d\n", (int)status):(int)status,不能写成*(int*)status,linux这里是64位机,8个字节,这样写会越界,而我们只需要一个整数而已。

    pthread_join:需要处理代码异常的问题吗??不需要!

    线程终止

    线程终止有三种方法:

    我们只介绍方法2和方法3: 

    pthread_exit

    void* retval:退出参数 ,给pthread_join的输出参数一个返回值。echo $?不受retval影响。

    代码测试:

    1. void* thread_run(void* args)
    2. {
    3. int id = *(int*)args;
    4. while(1)
    5. {
    6. printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
    7. sleep(5);
    8. break;
    9. }
    10. pthread_exit((void*)123);
    11. }
    12. #define NUM 1
    13. int main()
    14. {
    15. pthread_t tid[NUM];
    16. int i = 0;
    17. for(i = 0; i < NUM; i++)
    18. {
    19. pthread_create(tid+i, NULL, thread_run, (void*)&i);
    20. sleep(1);
    21. }
    22. void* status = NULL;
    23. for(i = 0; i < NUM; i++)
    24. {
    25. pthread_join(tid[i], &status); //阻塞式等待
    26. }
    27. printf("ret: %d\n", (int)status);
    28. sleep(3);
    29. return 0;
    30. }

    运行结果:

    我们发现通过进程等待可以拿到pthread_exit的退出结果。 

    pthread_exit和exit

    依照上面的代码,将pthread_exit改为exit:

    测试结果:

     

            我们发现退出结果没有打印,也就是说,在新线程中遇到exit后,整个进程(所有线程)全部退出。所以我们以后要想终止某一个线程就使用pthread_exit,而不是exit。

    终止线程的第三个方法:

    pthread_cancel

    pthread_t thread:要分离线程的id 

    返回值:成功就返回0,失败1返回退出码。

    函数调用成功,退出码为-1。

    这个-1是什么呢?

    [cyq@VM-0-7-centos 线程控制]$ grep -ER "PTHREAD_CANCELED" /usr/include/pthread.h 

     实际上就是pthread_exit的退出结果,-1就是宏:PTHREAD_CANCELED。

    举个栗子:

    1. void* thread_run(void* args)
    2. {
    3. int id = *(int*)args;
    4. while(1)
    5. {
    6. printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
    7. sleep(1);
    8. }
    9. }
    10. #define NUM 5
    11. int main()
    12. {
    13. pthread_t tid[NUM];
    14. int i = 0;
    15. for(i = 0; i < NUM; i++)
    16. {
    17. pthread_create(tid+i, NULL, thread_run, (void*)&i);
    18. sleep(1);
    19. }
    20. printf("wait sub thread...\n");
    21. int val = pthread_cancel(tid[0]);
    22. printf("cancel thread...\n");
    23. void* status = NULL;
    24. pthread_join(tid[0], &status); //阻塞式等待
    25. printf("val: %d\n", (int)status);
    26. sleep(3);
    27. return 0;
    28. }

    我们用一个命令脚本来看一下结果:

    [cyq@VM-0-7-centos test]$ while :; do ps -aL | head -1 && ps -aL | grep mytest; sleep 1; echo "###########################"; done
    

    我们让一个线程被pthread_cancel后,通过监视,发现该线程确实退出了。同时join到取消线程的退出结果是-1,说明取消成功了。

    注意

    pthread_cancel主线程可以取消子线程,但是子线程不能取消主线程;主线程也不能取消自己,否则会造成(该进程会成为僵尸进程)内存泄漏的问题。

    线程分离

    主线程要等待创建的新线程,否则会造成内存泄漏的问题,如果不想等待呢?有没有办法?

    pthread_detach 

    pthread_thread:要分离线程的id 

    注意:

     举个栗子:

    1. void *thread_run(void *args)
    2. {
    3. int id = *(int *)args;
    4. int cnt = 3;
    5. while (cnt--)
    6. {
    7. printf("我是[%d]线程, 我的线程id是: %lu\n", id, pthread_self());
    8. sleep(1);
    9. }
    10. }
    11. int main()
    12. {
    13. pthread_t tid;
    14. pthread_create(&tid, NULL, thread_run, (void*)"new thread");
    15. sleep(3);
    16. pthread_detach(tid);
    17. int ret = pthread_join(tid, NULL); //阻塞式等待
    18. printf("ret: %d\n", ret);
    19. sleep(3);
    20. return 0;
    21. }

    运行结果:

    我们发现pthread_join的等待结果不是0,说明等待线程失败了。同时最后线程都退出时我们发现没有<default>这样的标志,说明没有僵尸进程,所以不存在内存泄漏。 

    线程id和LWP

    线程id代码打印:

    1. void* thread_run(void* args)
    2. {
    3. int num = *(int*)args;
    4. while(1)
    5. {
    6. printf("我是[%d]线程, 我的线程ID是: %lu\n", num, pthread_self());
    7. sleep(1);
    8. }
    9. }
    10. int main()
    11. {
    12. pthread_t tid[5];
    13. printf("我是主线程, 线程id: %lu\n", pthread_self());
    14. for(int i = 0; i < 5; i++)
    15. {
    16. sleep(1);
    17. pthread_create(tid+i, NULL, thread_run, (void*)&i);
    18. }
    19. sleep(3);
    20. return 0;
    21. }

    注意:pthread_self要对应%lu打印。

    打印结果:

     我们发现同样是用来标识线程id的,但为什么两者差别这么大呢?

     我们用%x打印线程id:

                   

           我们查看到的线程id是pthread库中的线程id,不是linux内核中的LWP,pthread库的线程id是一个内存地址(虚拟地址)! ! 

    如何理解线程tid?

    实际上tid就是对应地址空间上共享区里的地址。因为用地址来做id,是可以最快找到映射的对应线程库的代码和数据。 

    用户级线程和内核轻量级线程的关系 

     大部分的操作系统都是这样设计的,不排除特别情况。

    看到这里,支持博主一下吧~

                                   

  • 相关阅读:
    Vue中事件总线EventBus的应用(三)定义全局事件——实例之main.js-创建事件总线、$emit-发布事件、$on-订阅事件、$off-去除事件
    Redis的String常用命令
    内存调试工具valgrind的使用
    ECMASript 6 新特性
    systemd 服务脚本编写与管理
    [计算机提升] 计算机进阶概念:路径
    无线智能振弦采集系统(NLM5或6多通道无线采集采发仪)
    Python Flask框架-开发简单博客-开篇介绍
    计算机毕业设计Java众筹平台网站(源码+系统+mysql数据库+lw文档)
    【Linux】缓冲区
  • 原文地址:https://blog.csdn.net/qq_58724706/article/details/125365533