• linux之线程


    关于线程和进程的区别:进程和线程的详解和区别_StudyWinter的博客-CSDN博客_任务进程线程的区别

    1 什么是线程

    (1)轻量级进程(light-weight process),也有 PCB,创建线程使用的底层函数和进程一样,都是 clone;

    (2)从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB 中指向内存资源的三级页表是相同的(下图区别进程)

    (3)进程可以蜕变成线程;

    (4)线程可看做寄存器和栈的集合;

    (5)在 linux 下,线程最是小的执行单位;进程是最小的分配资源单位

    参考:《Linux 内核源代码情景分析》
    对于进程来说,相同的地址(同一个虚拟地址)在不同的进程中,反复使用而不冲突。原因是他们虽虚拟址一样,但,页目录、页表、物理页面各不相同。相同的虚拟址,映射到不同的物理页面内存单元,最终访问不同的物理页面。

    但!线程不同!两个线程具有各自独立的 PCB,但共享同一个页目录,也就共享同一个页表和物理页面。所以两个 PCB 共享一个地址空间。实际上,无论是创建进程的 fork,还是创建线程的 pthread_create,底层实现都是调用同一个内核函数clone。


    如果复制对方的地址空间,那么就产出一个“进程”;如果共享对方的地址空间,就产生一个“线程”。因此:Linux 内核是不区分进程和线程的。只在用户层面上进行区分。所以,线程所有操作函数pthread_* 是库函数,而非系统调用。

    线程概念:

    1. 进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
    2. 线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
    3. ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
    ps -Lf 进程号 # 查看进程的线程

    打开浏览器

    2 线程共享资源

    1. 文件描述符表
    2. 每种信号的处理方式
    3. 当前工作目录
    4. 用户 ID 和组 ID
    5. 内存地址空间 (.text/.data/.bss/heap/共享库)

    3 线程间非共享资源

    1. 线程 id
    2. 处理器现场和栈指针(内核栈)
    3. 独立的栈空间(用户空间栈)
    4. errno 变量
    5. 信号屏蔽字
    6. 调度优先级

    4 线程的优缺点

    优点:
            1. 提高程序并发性
            2. 开销小
            3. 数据通信、共享数据方便
    缺点:
            1. 线程不稳定(第三方库函数实现)
            2. 线程调试困难
            3. 等待使用共享资源时造成程序运行速度变慢,主要是一些独占性的资源
            4. 线程的死锁,较长时间的等待或者资源竞争造成死锁

    5 线程控制原语

    编译的时候记得后面 -l pthread 毕竟第三方库实现。

    5.1 pthread_self 函数

    作用:获取线程 ID。其作用对应进程中 getpid() 函数。

    1. pthread_t pthread_self(void);
    2. // 成功返回本线程id

    5.2 pthread_create 函数

    创建一个新线程,其作用,对应进程中fork()函数。

    1. int pthread_create(pthread_t *thread,
    2. const pthread_attr_t *attr,
    3. void *(*start_routine) (void *),
    4. void *arg);
    5. // 成功返回0,失败返回errno
    6. // 参数一:表示传出参数,表示创建的子线程id
    7. // 参数二:线程属性,传NILL表使用默认属性
    8. // 参数三:函数指针,指向线程主函数(线程体),该函数运行结束,则线程结束。
    9. // 参数四:参数三函数的参数,空传NULL

    测试

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 子线程回调函数
    7. void *tfn(void *arg)
    8. {
    9. printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    10. return NULL;
    11. }
    12. int main(int argc, char *argv[])
    13. {
    14. pthread_t tid;
    15. printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    16. int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    17. if (res < 0)
    18. {
    19. perror("pthread_create error\n");
    20. exit(1);
    21. }
    22. return 0;
    23. }

    执行

    gcc test.c  -l pthread

    可以看到,子线程的打印信息并未出现。原因在于,主线程执行完之后,就销毁了整个进程的地址空间,于是子线程就无法打印。简单粗暴的方法就是让主线程睡1秒,等子线程执行。

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 子线程回调函数
    7. void *tfn(void *arg)
    8. {
    9. printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    10. return NULL;
    11. }
    12. int main(int argc, char *argv[])
    13. {
    14. pthread_t tid;
    15. printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    16. int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    17. if (res < 0)
    18. {
    19. perror("pthread_create error\n");
    20. exit(1);
    21. }
    22. sleep(1); // 在这里添加休眠
    23. return 0;
    24. }

    执行

    5.3 循环创建多个子线程

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 子线程回调函数
    7. void *tfn(void *arg)
    8. {
    9. int i = (int)arg;
    10. sleep(i);
    11. printf("-----I'm %d th thread: pid = %d, tid = %lu\n", i + 1,
    12. getpid(), pthread_self());
    13. return NULL;
    14. }
    15. int main(int argc, char *argv[])
    16. {
    17. int i;
    18. int res;
    19. pthread_t tid; // 线程id
    20. for (i = 0; i < 5; i++)
    21. {
    22. res = pthread_create(&tid, NULL, tfn, (void *)i); // 创建线程
    23. if (res != 0)
    24. {
    25. perror("pthread_create error\n");
    26. exit(1);
    27. }
    28. }
    29. printf("-------main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    30. sleep(i);
    31. return 0;
    32. }

    执行

    编译时会出现类型强转的警告。

    5.4 线程间全局变量

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. int var = 10;
    7. // 子线程回调函数
    8. void *tfn(void *arg)
    9. {
    10. var = 100;
    11. printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    12. return NULL;
    13. }
    14. int main(int argc, char *argv[])
    15. {
    16. printf("At first var = %d\n", var);
    17. pthread_t tid; // 子线程id
    18. int res = pthread_create(&tid, NULL, tfn, NULL);
    19. if (res != 0)
    20. {
    21. perror("pthread error\n");
    22. exit(1);
    23. }
    24. sleep(1);
    25. printf("After pthread_create, var = %d\n", var);
    26. return 0;
    27. }

    执行

    可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。

    5.5 pthread_exit退出

    作用:将单个线程退出。

    1. void pthread_exit(void *retval);
    2. // 参数:retval 表示线程退出状态,通常传 NULL

    比较

    1. exit(); // 退出当前进程。
    2. return: // 返回到调用者那里去。
    3. pthread_exit(): // 退出当前线程。

    重点

    在多线程的回调函数中加代码

    1 exit函数

    1. // 如果在回调函数里加一段代码:
    2. if(i == 2)
    3. {
    4. exit(0);
    5. }

     执行

    看起来好像是退出了第三个子线程,然而运行时,发现后续的4,5也没了。这是因为,exit是退出进程。

    2 return

    1. if(i == 2)
    2. {
    3. return NULL;
    4. }

    执行

    这样运行一下,发现后续线程不会凉凉,说明return是可以达到退出线程的目的。

    然而真正意义上,return是返回到函数调用者那里去,线程并没有退出。

    再修改一下,再定义一个函数func,直接返回那种

    1. void *func(void){
    2. return NULL;
    3. }
    4. if(i == 2)
    5. {
    6. func();
    7. }

    执行

    运行,发现1,2,3,4,5线程都还在,说明没有达到退出目的。

    再次修改

    1. void *func(void) {
    2. pthread_exit(NULL);
    3. return NULL;
    4. }
    5. if(i == 2)
    6. {
    7. func();
    8. }

    执行

    编译运行,发现3没了,看起来很科学的样子。
    pthread_exit表示将当前线程退出。放在函数里,还是直接调用,都可以。

    5.6 pthread_join 函数(重)

    作用:阻塞等待线程退出,获取线程退出状态其作用,对应进程中 waitpid() 函数;

    补充:任意线程得到其他线程的pid都可以回收,没有父线程回收子线程的说法。而进程需要父进程回收子进程。

    1. int pthread_join(pthread_t thread, void **retval);
    2. // 阻塞 回收线程。
    3. // thread: 待回收的线程id
    4. // retval:传出参数。 回收的那个线程的退出值。
    5. // 线程异常借助,值为 -1。
    6. // 返回值:成功:0
    7. // 失败:errno

    下面这个是回收线程并获取子线程返回值的小例子:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. struct thrd
    7. {
    8. int var;
    9. char str[256];
    10. };
    11. // 回调函数
    12. void *tfn(void *arg)
    13. {
    14. struct thrd *tval;
    15. tval = malloc(sizeof(struct thrd)); // 申请空间
    16. tval->var = 100;
    17. strcpy(tval->str, "hello thread"); // 拷贝数据
    18. return (void *)tval;
    19. }
    20. int main(int argc, char *argv[])
    21. {
    22. pthread_t tid;
    23. struct thrd *retval;
    24. int res = pthread_create(&tid, NULL, tfn, NULL);
    25. if (res != 0)
    26. {
    27. perror("pthread_create error\n");
    28. exit(1);
    29. }
    30. res = pthread_join(tid, (void **)(&retval));
    31. if (res != 0)
    32. {
    33. perror("pthread_join error\n");
    34. exit(1);
    35. }
    36. printf("Child thread exit with var = %d, str = %s\n", retval->var, retval->str);
    37. pthread_exit(NULL);
    38. }

    执行

    使用pthread_join函数将循环创建的多个子线程回收

    这里tid要使用数组来存

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. int var = 100;
    7. void *tfn(void *arg)
    8. {
    9. int i;
    10. i = (int)arg;
    11. if (i == 1)
    12. {
    13. var = 111;
    14. printf("I'm %dth pthread tid = %lu, var = %d\n", i,
    15. pthread_self(), var);
    16. return (void *)var;
    17. }
    18. else if (i == 3)
    19. {
    20. var = 333;
    21. printf("I'm %dth pthread tid = %lu, var = %d\n", i,
    22. pthread_self(), var);
    23. return (void *)var;
    24. }
    25. else
    26. {
    27. printf("I'm %dth pthread tid = %lu, var = %d\n", i,
    28. pthread_self(), var);
    29. return (void *)var;
    30. }
    31. return NULL;
    32. }
    33. int main(int argc, char *argv[])
    34. {
    35. pthread_t tid[5];
    36. int i;
    37. int *res[5];
    38. // 循环创建多个子进程
    39. for (i = 0; i < 5; i++)
    40. {
    41. pthread_create(&tid[i], NULL, tfn, (void *)i);
    42. }
    43. // 循环回收多个子进程
    44. for (i = 0; i < 5; i++)
    45. {
    46. pthread_join(tid[i], (void **)(&res[i]));
    47. printf("--------------%d 's res = %d\n", i, (int)(res[i]));
    48. }
    49. // 输出主线程
    50. printf("I'm main pthread tid = %lu, var = %d\n", pthread_self(), var);
    51. return 0;
    52. }

    执行

    5.7 pthread_cancel函数

    作用:杀死(取消)线程,其作用,对应进程中 kill() 函数。

    线程的取消并不是实时的,而有一定的延时。需要等待线程到达某个取消点(检查点)。


    取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用
    creat,open,pause, close,read,write… 执行命令 man 7 pthreads
    可以查看具备这些取消点的系统调用列表。也可参阅 APUE.12.7 取消选项小节。可粗略认为一个系统调用(进入内核)即为一个取消点。

    如线程中没有取消点,可以通过调pthread_testcancel函数自行设置一个取消点。

    1. int pthread_cancel(pthread_t thread);
    2. // 杀死一个线程。 需要到达取消点(保存点)
    3. // thread: 待杀死的线程id
    4. // 返回值:成功:0
    5. // 失败:errno

    如果,子线程没有到达取消点, 那么 pthread_cancel 无效。我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。

    小例子,主线程调用pthread_cancel杀死子线程

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 回调函数
    7. void *tfn(void *arg)
    8. {
    9. while (1)
    10. {
    11. printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    12. sleep(1);
    13. }
    14. }
    15. int main(int argc, char *argv[])
    16. {
    17. pthread_t tid;
    18. int res = pthread_create(&tid, NULL, tfn, NULL); // 创建线程
    19. if (res != 0)
    20. {
    21. fprintf(stderr, "pthread_create error:%s\n", strerror(res));
    22. exit(1);
    23. }
    24. printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    25. sleep(5); // 父线程睡5s
    26. res = pthread_cancel(tid); // 终止线程
    27. if (res != 0)
    28. {
    29. fprintf(stderr, "pthread_cancel error:%s\n", strerror(res));
    30. exit(1);
    31. }
    32. while(1); // 不退出
    33. pthread_exit((void *)0);
    34. }

    执行

    可以看到,主线程确实kill了子线程。
    这里要注意一点,pthread_cancel工作的必要条件是进入内核,如果tfn真的奇葩到没有进入内核,则pthread_cancel不能杀死线程,此时需要手动设置取消点,就是pthread_testcancel()

    5.8 pthread_detach 函数

    作用:实现线程分离,线程结束后,自动释放资源。无需pthread_join() 回收资源。

    1. int pthread_detach(pthread_t thread);
    2. // 设置线程分离
    3. // thread: 待分离的线程id
    4. // 返回值:成功:0
    5. // 失败:errno

    线程分离状态:指定该状态,线程主动与主控线程断开关系。

    线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。进程若有该机制,将不会产生僵尸进程。

    僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。也可使用 pthread_create 函数参 2(线程属性)来设置线程分离。

    下面这个例子,使用detach分离线程,照理来说,分离后的线程会自动回收:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 子线程回调函数
    7. void *tfn(void *arg)
    8. {
    9. printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    10. return NULL;
    11. }
    12. int main(int argc, char *argv[])
    13. {
    14. pthread_t tid;
    15. int res = pthread_create(&tid, NULL, tfn, NULL); // 创建一个线程
    16. if (res != 0)
    17. {
    18. // printf("pthread_create error:%s\n", strerror(res));
    19. fprintf(stderr, "pthread_create error:%s\n", strerror(res));
    20. exit(1);
    21. }
    22. res = pthread_detach(tid); // 设置线程分离,分离完的程序可以自动回收
    23. if (res != 0)
    24. {
    25. // printf("pthread_detach error:%s\n", strerror(res));
    26. fprintf(stderr, "pthread_detach error:%s\n", strerror(res));
    27. exit(1);
    28. }
    29. sleep(1);
    30. res = pthread_join(tid, NULL); // 回收子线程
    31. printf("join res = %d\n", res);
    32. if (res != 0)
    33. {
    34. // printf("pthread_join error:%s\n", strerror(res));
    35. fprintf(stderr, "pthread_join error:%s\n", strerror(res));
    36. exit(1);
    37. }
    38. printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    39. pthread_exit((void *)0);
    40. }

    这里是最终版,使用fprintf函数和strerror函数。

    执行

    6 线程进程控制原语比对

    进程线程
    forkpthread_create
    exitpthread_exit
    waitpthread_join
    killpthread_cancel
    getpidpthread_self
     pthread_detach()

    7 线程分离属性设置

    线程属性:
    设置分离属性。

    1. pthread_attr_t attr;
    2. // 创建一个线程属性结构体变量
    3. pthread_attr_init(&attr);
    4. // 初始化线程属性
    5. pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    6. // 设置线程属性为分离态
    7. pthread_create(&tid, &attr, tfn, NULL);
    8. // 借助修改后的 设置线程属性 创建为分离态的新线程
    9. pthread_attr_destroy(&attr);
    10. // 销毁线程属性

    调整线程状态,使线程创建出来就是分离态,代码如下:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <string.h>
    4. #include <unistd.h>
    5. #include <pthread.h>
    6. // 子线程回调函数
    7. void *tfn(void *arg)
    8. {
    9. printf("pthread: pid = %d, tid = %lu\n", getpid(), pthread_self());
    10. return NULL;
    11. }
    12. int main(int argc, char *argv[])
    13. {
    14. pthread_t tid;
    15. pthread_attr_t attr; // 结构体变量
    16. int res = pthread_attr_init(&attr); // 创建分离
    17. if (res != 0)
    18. {
    19. fprintf(stderr, "pthread_attr_init error :%s\n", strerror(res));
    20. exit(1);
    21. }
    22. if(res != 0)
    23. {
    24. fprintf(stderr, "pthread_attr_init error :%s\n",
    25. strerror(res));
    26. exit(1);
    27. }
    28. res = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    29. // 设置线程属性为分离属性
    30. if (res != 0)
    31. {
    32. fprintf(stderr, "pthread_attr_setdetachstate error :%s\n",
    33. strerror(res));
    34. exit(1);
    35. }
    36. res = pthread_create(&tid, &attr, tfn, NULL); // 创建一个线程
    37. if (res != 0)
    38. {
    39. fprintf(stderr, "pthread_create error: %s\n",
    40. strerror(res));
    41. exit(1);
    42. }
    43. res = pthread_attr_destroy(&attr); // 回收分离
    44. if (res != 0)
    45. {
    46. fprintf(stderr, "pthread_attr_destroy error :%s\n",
    47. strerror(res));
    48. exit(1);
    49. }
    50. sleep(1); // 保证子进程结束
    51. res = pthread_join(tid, NULL); // 阻塞回收,分离成功,这里应该回收失败
    52. if (res != 0)
    53. {
    54. fprintf(stderr, "pthread_join error :%s\n", strerror(res));
    55. exit(1);
    56. }
    57. printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
    58. pthread_exit((void *)0);
    59. }

     执行

    如图,pthread_join报错,说明线程已经自动回收,设置分离成功。

    8 线程属性注意事项(重)

    1 主线程退出其他线程不退出,主线程应调用 pthread_exit

    2 避免僵尸线程
    pthread_join
    pthread_detach
    pthread_create 指定分离属性
    被 join 线程可能在 join 函数返回前就释放完自己的所有内存资源,所以不应当返回被回收线程栈中的值;

    3 malloc 和 mmap 申请的内存可以被其他线程释放

    4 应避免在多线程模型中调用 fork 除非,马上 exec,子进程中只有调用 fork 的线程存在,其他线程在子进程中均 pthread_exit

    5 信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制。

  • 相关阅读:
    一心为农,以智取胜——张继群的智慧农业之路
    机器学习__03__机器学习之线性回归
    LeetCode 494 目标和
    Metabase的基本使用:10分钟快速入门
    编译原理复习——语法分析(自顶向下)
    R统计绘图-线性混合效应模型详解(理论、模型构建、检验、选择、方差分解及结果可视化)
    经典链表问题:解析链表中的关键挑战
    网络爬虫的意义:连接信息世界的纽带
    18 SpringMVC实战
    【数组】-找出有序数组中(有负有正)绝对值最小的数
  • 原文地址:https://blog.csdn.net/Zhouzi_heng/article/details/125405586