• 14.linux线程


    目录

    一、线程的基本概念

    什么是线程?

    线程的特点

    多进程与多线程

    二、创建线程

            pthread_create()函数

            pthread_exit()函数

    三、回收线程 

             pthread_join()函数

    四、取消线程

     pthread_cancel()函数

     pthread_setcancelstate()函数

    pthread_setcanceltype()函数

    五、分离线程 

    六、注册线程清理处理函数


    一、线程的基本概念

    什么是线程?

            线程是参与系统调度的最小单位,它被包含在进程之中, 是进程中的实际运行单位。一个进程中可以创建多个线程, 多个线程实现并发运行, 每个线程执行不同的任务。

    线程的特点

            线程是程序最基本的运行单位,而进程不能运行, 真正运行的是进程中的线程。 当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器, 它包含了线程运行所需的数据结构、环境变量等信息。

    线程不单独存在、而是包含在进程中;
    (1)线程是参与系统调度的基本单位;
    (2)可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;
    (3)共享进程资源。 同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。

    多进程与多线程

    一个进程可以通过fork()函数等创建子进程去实现并发处理多任务的功能,多个子进程的本质是单线程进程,一个进程中包含多个线程同样也可以实现并发处理的功能。

    多线程编程劣势:

    ① 进程间的切换开销比较大。
    ② 进程间的通信较为麻烦,因为多个进程通常位于自己独立的地址空间。

    多线程的优势:

    ① 同一进程中的多个线程由于都存在与同一片地址空间,因此线程间切换开销比较小。
    ② 同一进程中多个线程通信比较容易。
    ③ 线程创建的速度远快于进程。

    二、创建线程

            pthread_create()函数

            运行一个程序时,创建的进程相当于一个单线程进程,这个线程可以称为主线程。在系统库函数中提供了pthread_create(),可以实现主线程创建新的子线程,函数原型如下:

    1. #include <pthread.h>
    2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    3. thread: pthread_t 类型指针, 当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。
    4. attr: pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性,关于线程属性将会在 11.8 小节介绍。如果将参数 attr 设置为 NULL, 那么表示将线程的所有属性设置为默认值,以此创建新线程。
    5. start_routine: 参数 start_routine 是一个函数指针,指向一个函数, 新创建的线程从start_routine()函数开始运行,该函数返回值类型为void *。
    6. arg: 传递给 start_routine()函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说
    7. 在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。 当然也可
    8. 将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine()函数。

            pthread_exit()函数

    将终止调用它的线程,其函数原型如下所示:

    1. #include <pthread.h>
    2. void pthread_exit(void *retval);
    3. 参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码.

            线程创建成功, 新线程就会加入到系统调度队列中,获取到 CPU 之后就会立马从 start_routine()函数开始运行该线程的任务。

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void *new_thread(void *arg)
    9. {
    10. printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
    11. pthread_exit(NULL);
    12. }
    13. int main(void)
    14. {
    15. pthread_t tid;
    16. int ret;
    17. ret = pthread_create(&tid, NULL, new_thread, NULL);
    18. if (ret) {
    19. fprintf(stderr, "Error: %s\n", strerror(ret));
    20. exit(-1);
    21. }
    22. printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
    23. sleep(1);
    24. exit(0);
    25. }

    运行结果如下:

    从图中可以看出,两个线程的进程号相同,这是因为它们都属于同一个进程。

    三、回收线程 

             pthread_join()函数

            在进程中,父进程通常需要使用wait()函数,等待子进程结束后回收其资源。对于线程来说,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码, 回收线程资源,其函数原型如下:

    1. #include <pthread.h>
    2. int pthread_join(pthread_t thread, void **retval);
    3. 函数参数和返回值含义如下:
    4. thread: pthread_join()等待指定线程的终止,通过参数 thread(线程 ID) 指定需要等待的线程;
    5. retval: 如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过
    6. pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到*retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消, 则将 PTHREAD_CANCELED 放在*retval 中。
    7. 返回值: 成功返回 0;失败将返回错误码。

    ##由于同一个进程中任意线程的关系是对等的。所以进程中的任意线程都可以调用此函数等待另一个线程的结束## 

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void *new_thread(void *arg)
    9. {
    10. printf("新线程 start\n");
    11. sleep(2);
    12. printf("新线程 end\n");
    13. pthread_exit((void *)20); //线程返回值,通过tret获取。
    14. }
    15. int main(void)
    16. {
    17. pthread_t tid; // 保存线程号
    18. void *tret; //线程返回值
    19. int ret;
    20. ret = pthread_create(&tid, NULL, new_thread, NULL);
    21. if(ret) { //创建线程失败,在标准错误打印错误码
    22. fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
    23. exit(-1);
    24. }
    25. ret = pthread_join(tid, &tret); //回收线程
    26. if (ret) { //线程回收失败
    27. fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    28. exit(-1);
    29. }
    30. printf("新线程终止, code=%ld\n", (long)tret);
    31. exit(0);
    32. }

    主线程调用 pthread_create()创建新线程之后,新线程执行 new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止,新线程终止后, pthread_join()返回,将目标线程的退出码保存在*tret 所指向的内存中。

    运行结果如下:

    四、取消线程

    pthread_cancel()函数

            一个线程可以通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

    1. #include <pthread.h>
    2. int pthread_cancel(pthread_t thread);
    3. 发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程
    4. 也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1) 的
    5. pthread_exit()函数

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void *new_thread(void *arg)
    9. {
    10. printf("新线程 start\n");
    11. while(1) sleep(1);
    12. pthread_exit((void *)20); //线程返回值,通过tret获取。
    13. }
    14. int main(void)
    15. {
    16. pthread_t tid; // 保存线程号
    17. void *tret; //线程返回值
    18. int ret;
    19. ret = pthread_create(&tid, NULL, new_thread, NULL);
    20. if(ret) { //创建线程失败,在标准错误打印错误码
    21. fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
    22. exit(-1);
    23. }
    24. sleep(2);
    25. /* 向新线程发送取消请求 */
    26. ret = pthread_cancel(tid);
    27. if (ret) {
    28. fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
    29. exit(-1);
    30. }
    31. ret = pthread_join(tid, &tret); //回收线程
    32. if (ret) { //线程回收失败
    33. fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    34. exit(-1);
    35. }
    36. printf("新线程终止, code=%ld\n", (long)tret);
    37. exit(0);
    38. }

            主线程创建新线程,新线程 new_thread()函数直接运行死循环;主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用 pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。

    测试结果如下:

     pthread_setcancelstate()函数

            线程可以通过pthread_setcancelstate()函数设置为不会被取消,函数原型如下:

    1. #include <pthread.h>
    2. int pthread_setcancelstate(int state, int *oldstate);
    3. pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。
    4. 参数 state 必须是以下值之一:
    5. PTHREAD_CANCEL_ENABLE: 线程可以取消,这是新创建的线程取消性状态的默认值,所以
    6. 新建线程以及主线程默认都是可以取消的。
    7. PTHREAD_CANCEL_DISABLE: 线程不可被取消,如果此类线程接收到取消请求,则会将请求
    8. 挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void *new_thread(void *arg)
    9. {
    10. printf("新线程 start\n");
    11. pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    12. while(1) {
    13. printf("新线程 running\n");
    14. sleep(1);
    15. }
    16. pthread_exit((void *)20); //线程返回值,通过tret获取。
    17. }
    18. int main(void)
    19. {
    20. pthread_t tid; // 保存线程号
    21. void *tret; //线程返回值
    22. int ret;
    23. ret = pthread_create(&tid, NULL, new_thread, NULL);
    24. if(ret) { //创建线程失败,在标准错误打印错误码
    25. fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
    26. exit(-1);
    27. }
    28. sleep(2);
    29. /* 向新线程发送取消请求 */
    30. ret = pthread_cancel(tid);
    31. if (ret) {
    32. fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
    33. exit(-1);
    34. }
    35. ret = pthread_join(tid, &tret); //回收线程
    36. if (ret) { //线程回收失败
    37. fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    38. exit(-1);
    39. }
    40. printf("新线程终止, code=%ld\n", (long)tret);
    41. exit(0);
    42. }

            新线程 new_thread()函数中调用 pthread_setcancelstate()将自己设置为不可被取消,主线程延时2秒钟之后调用 pthread_cancel()向新线程发送取消请求,那么此时新线程是不会终止的, pthread_cancel()立刻返回之后进入到 pthread_join()函数,那么此时会被阻塞等待新线程终止。

            运行结果如下: 

    pthread_setcanceltype()函数

            如果线程的取消性状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型,而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果不需要之前的取消类型可以设置为NULL。

    1. #include <pthread.h>
    2. int pthread_setcanceltype(int type, int *oldtype);
    3. pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。
    4. 参数 type 必须是以下值之一:
    5. PTHREAD_CANCEL_DEFERRED: 取消请求到来时,线程还是继续运行,取消请求被挂起,直
    6. 到线程到达某个取消点(cancellation point,将在 11.6.3 小节介绍) 为止,这是所有新建线程包括
    7. 主线程默认的取消性类型。
    8. PTHREAD_CANCEL_ASYNCHRONOUS: 可能会在任何时间点(也许是立即取消,但不一定)

    如果需要查看哪些函数是取消点,可以通过man 7 pthreads命令进行查看。

    五、分离线程 

           当线程终止时,其它线程可以通过调用 pthread_join()获取其返回状态、回收线程资源。如果希望系统在线程终止时能够自动回收线程资源并将其移除,则可以调佣pthread_detach()函数:

    1. #include <pthread.h>
    2. int pthread_detach(pthread_t thread);

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void *new_thread_start(void *arg)
    9. {
    10. int ret;
    11. /* 分离自行分离 */
    12. ret = pthread_detach(pthread_self());
    13. if (ret) {
    14. fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
    15. return NULL;
    16. }
    17. printf("新线程 start\n");
    18. sleep(2); //休眠 2 秒钟
    19. printf("新线程 end\n");
    20. pthread_exit(NULL);
    21. }
    22. int main(void)
    23. {
    24. pthread_t tid;
    25. int ret;
    26. /* 创建新线程 */
    27. ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    28. if (ret) {
    29. fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
    30. exit(-1);
    31. }
    32. sleep(1); //休眠 1 秒钟
    33. /* 等待新线程终止 */
    34. ret = pthread_join(tid, NULL);
    35. if (ret)
    36. fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    37. pthread_exit(NULL);
    38. }

    主线程创建新的线程之后,休眠 1 秒钟,调用 pthread_join()等待新线程终止;新线程调用
    pthread_detach(pthread_self())将自己分离,休眠 2 秒钟之后 pthread_exit()退出线程;主线程休眠 1 秒钟是能够确保调用 pthread_join()函数时新线程已经将自己分离了,所以按照上面的介绍可知,此时主线程调用pthread_join()必然会失败。

    测试结果如下:

    六、注册线程清理处理函数

            当线程终止退出时,去执行处理函数,这个称为线程清理函数。线程通过函数 pthread_cleanup_push()和 pthread_cleanup_pop()分别负责向调用线程的清理函数栈中添加
    和移除清理函数,函数原型如下所示:

    1. #include <pthread.h>
    2. void pthread_cleanup_push(void (*routine)(void *), void *arg);
    3. void pthread_cleanup_pop(int execute);
    4. 调用 pthread_cleanup_push()向清理函数栈中添加一个清理函数,第一个参数 routine 是一个函数指
    5. 针,指向一个需要添加的清理函数, routine()函数无返回值,只有一个 void *类型参数;第二个参
    6. 数 arg,当调用清理函数 routine()时, 将 arg 作为 routine()函数的参数。
    7. 函数 pthread_cleanup_pop()的 execute 参数,可以取值为 0,也可以为非 0;如果为 0,清理函数
    8. 不会被调用,只是将清理函数栈中最顶层的函数移除;如果参数 execute 为非 0,则除了将清理函数
    9. 栈中最顶层的函数移除之外,还会该清理函数。

    测试代码:

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <pthread.h>
    4. #include <string.h>
    5. #include <unistd.h>
    6. #include <sys/types.h>
    7. #include <unistd.h>
    8. static void cleanup(void *arg)
    9. {
    10. printf("cleanup: %s\n", (char *)arg);
    11. }
    12. static void *new_thread_start(void *arg)
    13. {
    14. printf("新线程--start run\n");
    15. pthread_cleanup_push(cleanup, "第 1 次调用");
    16. pthread_cleanup_push(cleanup, "第 2 次调用");
    17. pthread_cleanup_push(cleanup, "第 3 次调用");
    18. pthread_cleanup_pop(1); //执行最顶层的清理函数
    19. printf("~~~~~~~~~~~~~~~~~\n");
    20. sleep(2);
    21. pthread_exit((void *)0); //线程终止
    22. /* 为了与 pthread_cleanup_push 配对 */
    23. pthread_cleanup_pop(0);
    24. pthread_cleanup_pop(0);
    25. }
    26. int main(void)
    27. {
    28. pthread_t tid;
    29. void *tret;
    30. int ret;
    31. /* 创建新线程 */
    32. ret = pthread_create(&tid, NULL, new_thread_start, NULL);
    33. if (ret) {
    34. fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
    35. exit(-1);
    36. }
    37. /* 等待新线程终止 */
    38. ret = pthread_join(tid, &tret);
    39. if (ret) {
    40. fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
    41. exit(-1);
    42. }
    43. printf("新线程终止, code=%ld\n", (long)tret);
    44. exit(0);
    45. }

            在新线程调用 pthread_exit()之前,先调用 pthread_cleanup_pop(1)手动运行了最顶层的清理
    函数,并将其从栈中移除,并且清理函数执行的顺序和栈是一样的,先进后出,测试结果:


     

  • 相关阅读:
    通过SSH 可以访问Ubuntu Desktop吗?
    求每个店铺访问次数top3的访客信息
    用Acceldata数据可观测性方案管理云数据平台Snowflake
    Leetcode刷题详解——二叉树剪枝
    【C++】多态 — 多态的原理 (下篇)
    Liunx 实时调度策略 SCHED_RR SCHED_FIFO 区别 适用情况
    算法通过村第十四关-堆|黄金笔记|中位数
    [vue]——vue3.0+高德地图的正确打开方式
    vue实战-完全掌握Vue自定义指令
    FCOS难点记录
  • 原文地址:https://blog.csdn.net/qq_42174306/article/details/125429472