Linux系统编程(九):线程(下)

2024-01-03 13:21:24

参考引用

1. 线程概述

1.1 线程定义

  • 线程是参与系统调度的最小单位,它被包含在进程之中,是进程中的实际运行单位
    • 一个线程指的是进程中一个单一顺序的控制流(或者说是执行路线、执行流)
    • 一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务
    • 如:某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中

1.2 线程是如何创建起来的?

  • 当一个程序启动时,就有一个进程被操作系统创建,同时一个线程也立刻运行,通常叫主线程(Main Thread)
    • 应用程序都是以 main() 做为入口开始运行的,所以 main() 函数就是主线程的入口函数,main() 函数所执行的任务就是主线程需要执行的任务
  • 任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用 pthread_create 创建),创建的新线程就是主线程的子线程
    • 其它新的线程(也就是子线程)由主线程创建
    • 主线程通常会在最后结束运行,执行各种清理工作,如:回收各个子线程

1.3 线程特点

  • 线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程
    • 当启动应用程序后,系统就创建了一个进程
    • 可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息
  • 同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack,称为线程栈)、寄存器环境及线程本地存储
  • 线程不能单独存在,而是包含在进程中
  • 同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果

1.4 线程与进程对比

  • 进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程)并发处理多任务的需求,多进程和多线程两种编程模型的优势和劣势如下
  • 1. 多进程编程的劣势
    • 进程间切换开销大
      • 多个进程同时运行(宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算
    • 进程间通信较为麻烦
      • 每个进程都在各自的地址空间中,相互独立、隔离,因此相互通信较为麻烦
  • 2. 多线程编程的优势
    • 同一进程的多个线程间切换开销比较小
    • 同一进程的多个线程间通信容易
      • 它们共享了进程的地址空间,所以在同一个地址空间中
    • 线程创建的速度远大于进程创建的速度
    • 多线程在多核处理器上更有优势
  • 3. 对比
    • 多线程编程难度高,在多线程环境下需要考虑很多的问题,如:线程安全问题、信号处理的问题等,编写与调试一个多线程程序比单线程程序困难得多
    • 多进程编程通常会用在一些大型应用程序项目中,如:网络服务器应用程序

1.5 并发和并行

1.5.1 串行
  • 串行指的是一种顺序执行,如:先完成 task1,接着做 task2、直到完成 task2,然后做 task3、直到完成 task3…… 依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行
    在这里插入图片描述
1.5.2 并行
  • 并行指的是可以并排/并列执行多个任务,这样的系统通常有多个执行单元可以实现并行运行,如:并行运行 task1、task2、task3
    在这里插入图片描述

  • 并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着
    在这里插入图片描述

1.5.3 并发
  • 相比于串行和并行,并发强调的是一种分时复用

    • 分时复用:不必等待上一个任务完成之后再做下一个任务,可以打断当前执行的任务切换执行下一个任务
    • 在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行
      在这里插入图片描述
  • 总结

    • 串行:一件事、一件事接着做
    • 并行:同时做不同的事(并行运行情况下的多个执行单元,每一个执行单元同样也可以并发运行)
    • 并发:交替做不同的事

多核处理器和单核处理器

  • 对于单核处理器来说,只有一个执行单元,同时只能执行一条指令
  • 对于多核处理起来说,有多个执行单元,在操作系统中,多个执行单元以并行方式运行多个进程,同时每一个执行单元以并发方式运行系统中的多个线程(进程级并行和线程级并发)
  • 在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替/交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程

2. 线程 ID

  • 每个线程有其对应的标识,称为线程 ID

    • 进程 ID 在整个系统中是唯一的
    • 但线程 ID 只有在它所属的进程上下文中才有意义
  • 进程 ID 使用 pid_t 数据类型来表示(非负整数),而线程 ID 使用 pthread_t 数据类型(unsigned long int)表示

    • 一个线程可通过库函数 pthread_self() 来获取自己的线程 ID
    #include <pthread.h>
    
    pthread_t pthread_self(void);
    
    • 可以使用 pthread_equal() 函数来检查两个线程 ID 是否相等
    #include <pthread.h>
    
    // 如果两个线程 ID t1 和 t2 相等,则 pthread_equal() 返回一个非零值;否则返回 0
    int pthread_equal(pthread_t t1, pthread_t t2);
    

3. 创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  • 主线程使用库函数 pthread_create() 创建一个新的线程,称为主线程的子线程

    • thread:pthread_t 类型指针
      • 当 pthread_create() 成功返回时,新创建线程的线程 ID 会保存在参数 thread 所指向的内存中,后续的线程相关函数会使用该标识来引用此线程
    • attr:pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区
      • pthread_attr_t 数据类型定义了线程的各种属性
      • 若 attr 设置为 NULL,表示将线程的所有属性设置为默认值,以此创建新线程
    • start_routine:是一个函数指针,指向一个函数
      • 新创建的线程从 start_routine() 函数开始运行,返回值类型为 void*,参数只有一个 void*,其实这个参数就是 pthread_create() 函数的第四个参数 arg
      • 如果需要向 start_routine() 传递的参数个数大于 1,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入
    • arg:传递给 start_routine() 函数的参数
      • 一般情况下,需要将 arg 指向一个全局或堆变量,也就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误
      • 也可将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine() 函数
    • 返回值
      • 成功返回 0
      • 失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的
  • 线程创建成功,新线程就会加入到系统调度队列中,获取到 CPU 之后就会立马从 start_routine() 函数开始运行该线程的任务

    • 调用 pthread_create() 函数后,通常无法确定系统接着会调度哪一个线程来使用 CPU 资源,无法确定先调度主线程还是新创建的线程(而在多核 CPU 或多 CPU 系统中,多核线程可能会在不同的核心上同时执行
    • 如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现
  • 示例: pthread_create() 创建线程

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <pthread.h>
    
    static void *new_thread_start(void *arg) {
        printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
        return (void *)0;
    }
    
    int main(void) {
        pthread_t tid;
        int ret;
    
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "Error: %s\n", strerror(ret));
            exit(-1);
        }
    
        printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
    
        // 主线程休眠 1 秒钟,如果主线程不进行休眠,它就会立马退出
        // 这样可能会导致新创建的线程还没有机会运行,整个进程就结束了
        sleep(1);
        exit(0);
    }
    
    # 使用 -l 选项指定链接库 pthread,原因在于 pthread 不在 gcc 的默认链接库中,所以需要手动指定
    $ gcc thread.c -o thread -l pthread
    yxd@yxd-VirtualBox:~/Desktop/test$ ./thread 
    # 新创建的线程与主线程属于同一个进程,但是它们的线程 ID 不同
    主线程: 进程 ID<2624> 线程 ID<140143689406272>
    新线程: 进程 ID<2624> 线程 ID<140143681070848>
    

4. 终止线程

  • 终止线程的方式

    • 线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码
    • 线程调用 pthread_exit() 函数
    • 调用 pthread_cancel() 取消线程

    如果进程中的任意线程调用 exit()、_exit() 或 _Exit(),那么将会导致整个进程终止

  • pthread_exit() 函数将终止调用它的线程

    #include <pthread.h>
    
    // 1. 参数 retval 的数据类型为 void*,指定了线程的返回值,也就是线程的退出码
    // 该返回值可由另一个线程通过调用 pthread_join() 来获取
    // 2. 参数 retval 所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效
    void pthread_exit(void *retval);
    
  • 调用 pthread_exit() 相当于在线程的 start 函数中执行 return 语句

    • 不同之处在于:可在线程 start 函数所调用的任意函数中调用 pthread_exit() 来终止线程
    • 如果主线程调用了 pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止
  • 示例:pthread_exit() 终止线程

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <pthread.h>
    
    static void *new_thread_start(void *arg) {
        printf("新线程 start\n");
        sleep(1); // 新线程中调用 sleep() 休眠,保证主线程先调用 pthread_exit() 终止
        printf("新线程 end\n");
        pthread_exit(NULL);
    }
    
    int main(void) {
        pthread_t tid;
        int ret;
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "Error: %s\n", strerror(ret));
            exit(-1);
        }
    
        printf("主线程 end\n");
        pthread_exit(NULL);
    
        exit(0);
    }
    
    $ gcc thread1.c -o thread1 -l pthread
    $ ./thread1 
    主线程 end
    新线程 start
    新线程 end
    

5. 回收线程

  • 在父、子进程当中,父进程可通过 wait() 函数(或 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中通过调用 pthread_join() 函数来阻塞等待线程终止,并获取线程的退出码以回收线程资源

    #include <pthread.h>
    
    // thread:pthread_join() 等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程
    // retval:如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态复制到 *retval 所指向的内存区域
            // 如果目标线程被 pthread_cancel() 取消,则将 PTHREAD_CANCELED 放在 *retval 中
            // 如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL
    // 返回值:成功返回 0;失败将返回错误码
    int pthread_join(pthread_t thread, void **retval);
    
  • 调用 pthread_join() 函数将会以阻塞的形式等待指定的线程终止

    • 如果该线程已经终止,则 pthread_join() 立刻返回
    • 如果多个线程同时尝试调用 pthread_join() 等待指定线程的终止,那么结果将是不确定的
    • 若线程并未分离(detached),则必须使用 pthread_join() 来等待线程终止,回收线程资源
    • 如果线程终止后,其它线程没有调用 pthread_join() 函数来回收该线程,那么该线程将变成僵尸线程
    • 僵尸线程除了浪费系统资源外,若僵尸线程积累过多会导致应用程序无法创建新的线程
    • 如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收
  • pthread_join() 执行的功能类似于针对进程的 waitpid() 调用,不过存在一些显著差别

    • 线程之间关系是对等的
      • 进程中的任意线程均可调用 pthread_join() 函数来等待另一个线程的终止:如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join() 等待线程 C 的终止,线程 C 也可以调用 pthread_join() 等待线程 A 的终止
      • 这与进程间层次关系不同,父进程如果使用 fork() 创建子进程,那么它也是唯一能够对子进程调用 wait() 的进程,线程之间不存在这样的关系
    • 不能以非阻塞的方式调用 pthread_join()
      • 对于进程,调用 waitpid() 既可以实现阻塞方式等待,也可以实现非阻塞方式等待
  • 示例:pthread_join() 等待线程终止

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <pthread.h>
    
    // 主线程调用 pthread_create() 创建新线程之后,新线程执行 new_thread_start()函数
    static void *new_thread_start(void *arg) {
        printf("新线程 start\n");
        sleep(2);
        printf("新线程 end\n");
        pthread_exit((void *)10);
    }
    
    int main(void) {
        pthread_t tid;
        void *tret;
        int ret;
    
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "pthread_cread error: %s\n", strerror(ret));
            exit(-1);
        }
        
        // 主线程中调用 pthread_join() 阻塞等待新线程终止,新线程终止后,pthread_join() 返回
        // 将目标线程的退出码保存在 *tret 所指向的内存中
        ret = pthread_join(tid, &tret);
        if (ret) {
            fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
            exit(-1);
        }
       
        printf("新线程终止, code=%ld\n", (long)tret);
    
        exit(0);
    }
    
    $ gcc thread2.c -o thread2 -l pthread
    $ ./thread2
    新线程 start
    新线程 end
    新线程终止, code=10
    

6. 取消线程

  • 在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit() 退出,或在线程 start 函数执行 return 语句退出
    • 有时需要向一个线程发送一个请求,要求它立刻退出,把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。如:一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了

6.1 取消一个线程

  • 通过调用 pthread_cancel() 库函数向一个指定的线程发送取消请求

    • 发出取消请求后,函数 pthread_cancel() 立即返回,不会等待目标线程的退出,仅仅只是提出请求
    #include <pthread.h>
    
    int pthread_cancel(pthread_t thread);
    
  • 示例:pthread_cancel() 取消线程使用

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <pthread.h>
    
    static void *new_thread_start(void *arg) {
        printf("新线程 start\n");
        for (;;) {
            sleep(1);
        }
    
        return (void *)0;
    }
    
    int main(void) {
        pthread_t tid;
        void *tret;
        int ret;
    
        /* 创建新线程 */
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "pthread_cread error: %s\n", strerror(ret));
            exit(-1);
        }
    
        sleep(1);
    
        /* 向新线程发送取消请求 */
        ret = pthread_cancel(tid);
        if (ret) {
            fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
            exit(-1);
        }
    
        /* 等待新线程终止 */
        ret = pthread_join(tid, &tret);
        if (ret) {
            fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
            exit(-1);
        }
    
        printf("新线程终止, code=%ld\n", (long)tret);
    
        exit(0);
    }
    
    $ gcc thread3.c -o thread3 -l pthread
    $ ./thread3
    新线程 start
    新线程终止, code=-1
    

6.2 取消状态以及类型

  • 默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消,通过 pthread_setcancelstate() 和 pthread_setcanceltype() 来设置线程的取消状态和类型
    #include <pthread.h>
    
    int pthread_setcancelstate(int state, int *oldstate);
    int pthread_setcanceltype(int type, int *oldtype);
    
6.2.1 pthread_setcancelstate() 函数
  • pthread_setcancelstate() 函数会将调用线程的取消性状态设置为参数 state 中给定的值,并将线程之前的取消性状态保存在参数 oldstate 指向的缓冲区中,如果对之前的状态不感兴趣,则将参数 oldstate 设置为 NULL

  • pthread_setcancelstate() 调用成功将返回 0,失败返回非 0 值的错误码

  • pthread_setcancelstate() 函数执行的设置取消状态和获取旧状态操作,这两步是一个原子操作

  • 参数 state 必须是以下值之一

    • PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的
    • PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE
  • 示例:修改 6.1 小节下述代码

    ...
    static void *new_thread_start(void *arg) {
        /* 设置为不可被取消 */
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    
        for (;;) {
            printf("新线程--running\n");
            sleep(2);
        }
    
        return (void *)0;
    }
    ...
    
    $ gcc thread4.c -o thread4 -l pthread
    $ ./thread4
    新线程--running
    新线程--running
    新线程--running
    ...
    
6.2.2 pthread_setcanceltype() 函数
  • 如果线程的取消状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消类型,该类型可以通过调用 pthread_setcanceltype() 函数来设置,它的参数 type 指定了需要设置的类型,而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果对之前的类型不敢兴趣,则将参数 oldtype 设置为 NULL
  • pthread_setcanceltype() 函数调用成功将返回 0,失败返回非 0 值的错误码
  • pthread_setcanceltype() 函数执行的设置取消类型和获取旧类型操作,这两步是一个原子操作
  • 参数 type 必须是以下值之一
    • PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型
    • PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消类型应用场景很少

6.3 取消点

  • 若将线程的取消类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用

  • 什么是取消点?

    • 所谓取消点其实就是一系列函数,当执行到这些函数时,才会真正响应取消请求,这些函数就是取消点
    • 在没有出现取消点时,取消请求是无法得到处理的,因为系统认为:在没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致发生异常
  • 取消点函数

    • 查看取消点函数
    # Cancellation points
    $ man 7 pthreads
    
    • 若将 6.1 小节代码修改如下,则永远无法取消,因为不存在取消点
    ...
    static void *new_thread_start(void *arg) {
        printf("新线程--running\n");
        for (;;) {
        
        }
    
        return (void *)0;
    }
    ...
    

在这里插入图片描述

6.4 线程可取消性检测

  • 如何解决上述由于不存在取消点导致无法取消请求的问题?

    • 使用 pthread_testcancel() 函数,产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止
    #include <pthread.h>
    
    void pthread_testcancel(void);
    
  • 示例:使用 pthread_testcancel() 产生取消点

    // 修改 6.1 小节下述代码
    ...
    static void *new_thread_start(void *arg) {
        printf("新线程--start run\n");
    
        for (;;) {
            pthread_testcancel();
        }
    
        return (void *)0;
    }
    ...
    
    $ gcc thread5.c -o thread5 -l pthread
    $ ./thread5
    新线程--start run
    新线程终止,code=-1
    

7. 分离线程

  • 默认情况下,当线程终止时,其它线程可以通过调用 pthread_join() 获取其返回状态并回收线程资源,有时并不关心线程的返回状态,只希望系统在线程终止时能够自动回收线程资源并将其移除

    • 在这种情况下,可以调用 pthread_detach() 将指定线程进行分离,也就是分离线程
    #include <pthread.h>
    
    int pthread_detach(pthread_t thread);
    
    • 一个线程既可以将另一个线程分离,同时也可以将自己分离
    • 一旦线程处于分离状态,就不能再使用 pthread_join() 来获取其终止状态,此过程是不可逆的,一旦处于分离状态便不能再恢复到之前的状态,处于分离状态的线程,当其终止后能够自动回收线程资源
  • 示例:pthread_detach() 分离线程使用

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <pthread.h>
    
    static void *new_thread_start(void *arg) {
        int ret;
    
        /* 自行分离 */
        ret = pthread_detach(pthread_self());
        if (ret) {
            fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
            return NULL;
        }
        
        printf("新线程 start\n");
        sleep(2);  // 休眠 2 秒钟
        printf("新线程 end\n");
        pthread_exit(NULL);
    }
    
    int main(void) {
        pthread_t tid;
        int ret;
    
        /* 创建新线程 */
        ret = pthread_create(&tid, NULL, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "pthread_cread error: %s\n", strerror(ret));
            exit(-1);
        }
    
        // 休眠 1 秒钟,确保调用 pthread_join() 函数时新线程已经将自己分离,此时主线程调用 pthread_join() 必然会失败
        sleep(1);  
    
        /* 等待新线程终止 */
        ret = pthread_join(tid, NULL);
        if (ret) {
            fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        }
    
        pthread_exit(NULL);
    }
    
    $ gcc thread4.c -o thread4 -l pthread
    $ ./thread4
    新线程 start
    pthread_join error: Invalid argument
    新线程 end
    

8. 线程属性

  • 调用 pthread_create() 创建线程,可对新建线程的各种属性进行设置,Linux 使用 pthread_attr_t 数据类型定义线程的所有属性

    • 调用 pthread_create() 创建线程时,参数 attr 设置为 NULL,表示使用属性的默认值创建线程
    • 如果不使用默认值,参数 attr 必须要指向一个 pthread_attr_t 对象
  • 当定义 pthread_attr_t 对象后,需要使用 pthread_attr_init() 函数对该对象进行初始化操作,当对象不再使用时,需要使用 pthread_attr_destroy() 函数将其销毁

    #include <pthread.h>
    
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);
    

8.1 线程栈属性

  • 每个线程都有自己的栈空间,pthread_attr_t 数据结构中定义了栈的起始地址以及栈大小

    • 函数 pthread_attr_getstack() 可以获取这些信息
    • 函数 pthread_attr_setstack() 对栈起始地址和栈大小进行设置
    #include <pthread.h>
    
    // attr:指向线程属性对象
    // stackaddr:栈起始地址信息保存在 *stackaddr 中
    // stacksize:栈大小信息保存在参数 stacksize 所指向的内存中
    int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
    
    // attr:指向线程属性对象
    // stackaddr:设置栈起始地址为指定值
    // stacksize:设置栈大小为指定值
    int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
    
  • 示例:创建新的线程并将线程栈大小设置为 4Kb

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <string.h>
    
    static void *new_thread_start(void *arg) {
        puts("Hello World!");
        return (void*)0;
    }
    
    int main(int argc, char *argv[]) {
        pthread_attr_t attr;
        size_t stacksize;
        pthread_t tid;
        int ret;
    
        /* 对 attr 对象进行初始化 */
        pthread_attr_init(&attr);
    
        /* 设置栈大小为 4K */
        pthread_attr_setstacksize(&attr, 4096);
    
        /* 创建新线程 */
        ret = pthread_create(&tid, &attr, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
            exit(-1);
        }
    
        /* 等待新线程终止 */
        ret = pthread_join(tid, NULL);
        if (ret) {
            fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
            exit(-1);
        }
    
        /* 销毁 attr 对象 */
        pthread_attr_destroy(&attr);
    
        exit(0);
    }
    
    $ gcc stack.c -o stack -l pthread
    $ ./stack 
    Hello World!
    

8.2 分离状态属性

  • 如果在创建线程时就确定要将该线程分离,可以修改 pthread_attr_t 结构中的 detachstate 线程属性,让线程一开始运行就处于分离状态

    • 函数 pthread_attr_setdetachstate() 设置 detachstate 线程属性
    • 函数 pthread_attr_getdetachstate() 获取 detachstate 线程属性
    #include <pthread.h>
    
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
    
  • 参数 detachstate 取值如下

    • PTHREAD_CREATE_DETACHED:新建线程一开始运行便处于分离状态,以分离状态启动线程,无法被其它线程调用 pthread_join() 回收,线程结束后由操作系统收回其所占用的资源
    • PTHREAD_CREATE_JOINABLE:这是 detachstate 线程属性的默认值,正常启动线程,可以被其它线程获取终止状态信息
  • 示例:以分离状态启动线程

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <string.h>
    #include <unistd.h>
    
    static void *new_thread_start(void *arg) {
        puts("Hello World!");
        return (void *)0;
    }
    
    int main(int argc, char *argv[]) {
        pthread_attr_t attr;
        pthread_t tid;
        int ret;
    
        /* 对 attr 对象进行初始化 */
        pthread_attr_init(&attr);
    
        /* 设置以分离状态启动线程 */
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
        /* 创建新线程 */
        ret = pthread_create(&tid, &attr, new_thread_start, NULL);
        if (ret) {
            fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
            exit(-1);
        }
    
        sleep(1);
    
        /* 销毁 attr 对象 */
        pthread_attr_destroy(&attr);
    
        exit(0);
    }
    
    $ gcc detach.c -o detach -l pthread
    $ ./detach 
    Hello World!
    

9. 线程与信号

  • 线程与信号之间存在一些冲突,原因在于:信号既要能够在传统的单线程进程中保持它原有的功能与特性,同时又需要设计出能够适用于多线程环境的新特性
    • 应尽量避免信号与多线程模型之间结合使用

9.1 信号如何映射到线程

  • 信号模型在一些方面属于进程层面(由进程中的所有线程共享),在另一些方面则属于单个线程层面
    • 信号的系统默认行为属于进程层面
      • 信号的默认操作通常是停止或终止进程
    • 信号处理函数属于进程层面
      • 进程中的所有线程共享程序中所注册的信号处理函数
    • 信号的发送既可针对整个进程,也可针对某个特定的线程,在满足以下三个条件中的任意一个时,信号的发送针对的是某个线程
      • 产生了硬件异常相关信号,如 SIGBUS、SIGFPE、SIGILL 和 SIGSEGV 信号
      • 当线程试图对已断开的管道进行写操作时所产生的 SIGPIPE 信号
      • 由函数 pthread_kill() 或 pthread_sigqueue() 所发出的信号,这些函数允许线程向同一进程下的其它线程发送一个指定的信号

    除了以上提到的三种情况外,其它机制产生的信号均属于进程层面,如:其它进程调用 kill() 或 sigqueue() 所发送的信号;用户在终端按下 Ctrl+C、Ctrl+\、Ctrl+Z 向前台进程发送的 SIGINT、SIGQUIT 以及 SIGTSTP 信号

    • 信号掩码其实是属于线程层面的,也就是说信号掩码是针对每个线程而言
    • 针对整个进程所挂起的信号,以及针对每个线程所挂起的信号,内核都会分别进行维护、记录

9.2 线程的信号掩码

  • 对于一个单线程程序来说,使用 sigprocmask() 函数设置进程的信号掩码,在多线程环境下,使用 pthread_sigmask() 函数来设置各个线程的信号掩码
    • 每个刚创建的线程,会从其创建者处继承信号掩码,这个新的线程可以调用 pthread_sigmask() 函数来改变它的信号掩码
    #include <signal.h>
    
    // pthread_sigmask() 函数用法与 sigprocmask() 完全一样
    int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
    

9.3 向线程发送信号

  • 多线程程序中,可通过 pthread_kill() 向同一进程中的某个指定线程发送信号

    #include <signal.h>
    
    int pthread_kill(pthread_t thread, int sig);
    
  • pthread_sigqueue() 也可向同一进程中的某个指定的线程发送信号

    #include <signal.h>
    #include <pthread.h>
    
    int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);
    

文章来源:https://blog.csdn.net/qq_42994487/article/details/135342887
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。