线程-I.MX6U嵌入式Linux C应用编程学习笔记基于正点原子阿尔法开发板

news2024/11/22 22:19:58

线程

线程概念

在这里插入图片描述

简介

  • 线程定义:线程是操作系统进行调度的最小单位,包含在进程内,是进程中的实际执行单元

  • 线程特性:一个线程代表进程中的一个单一顺序控制流,即执行路径

  • 多线程应用:一个进程可以包含多个线程,这些线程可以并发执行,每个线程处理不同的任务

  • 实际应用示例:例如,一个应用程序需要同时运行两个任务(task1和task2),可以将这两个任务分别放在两个不同的线程中实现并发执行

创建

  • 进程与线程的启动:程序启动时,操作系统创建一个进程,并同时运行一个线程,即主线程

  • 主线程的作用:主线程是程序启动后立即运行的线程,通常从main()函数开始执行,负责初始任务

  • 单线程与多线程进程:任何进程至少包含一个主线程,只有主线程的进程称为单线程进程。多线程进程除了主线程外,还包括由主线程创建的其他线程

  • 主线程的重要性:主线程负责创建其他线程(子线程),并在程序结束时进行清理工作,如回收子线程

特点

  • 线程与进程的关系:线程是程序的基本运行单位,进程本身不运行,而是提供线程运行的环境和资源

  • 进程的容器作用:进程包含线程运行所需的数据结构和环境变量等信息

  • 线程的资源共享:同一进程中的线程共享进程的系统资源,如虚拟地址空间和文件描述符

  • 线程的独立性:每个线程有自己的调用栈、寄存器环境和线程本地存储

  • 线程的特点

    • 线程存在于进程内部

    • 线程是系统调度的基本单位

    • 线程可以并发执行,实现宏观上的同时运行

    • 线程共享进程的资源,包括地址空间和其他进程资源

线程与进程

  • 并发处理的选择:进程创建子进程或多线程都可以实现并发处理多任务,需要根据具体情况选择

  • 多进程编程的劣势

    • 进程间切换开销大

      • 多个进程在宏观上看似同时运行,实际上是轮流切换执行,进程间的切换成本远高于同一进程内线程间的切换成本,对于中小型应用程序而言,这种开销通常不经济
    • 进程间通信复杂

      • 每个进程拥有独立的地址空间,相互隔离
  • 多线程编程的优势

    • 线程间切换开销小

    • 线程间通信容易

      • 它们共享了进程的地址空间
    • 线程创建速度快

    • 多线程在多核处理器上更有优势

  • 多线程编程的劣势

    • 编程难度高,要求程序员有较高的编程功底

    • 需要考虑线程安全和信号处理等问题

并发和并行

  • 串行:任务按顺序逐一完成,必须完成前一个任务才能开始下一个,只有一个执行单元

  • 并行:多个任务同时进行,系统有多个执行单元,可以同时处理多个任务

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

  • 并发:任务在时间上交替进行,不必等待前一个任务完成,可以打断当前执行的任务切换执行下一个任务,每个任务执行一段时间,时间一到则切换执行下一个任务,可以在同一个执行单元上轮流执行不同任务

  • 比喻

    • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并
      行,仅仅只是串行。

    • 你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发

    • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行

  • 并行与并发的结合:在多核处理器系统中,多个执行单元可以并行处理多个线程,每个执行单元也可以并发处理多个线程

  • 计算机系统中的应用:单核处理器只能并发运行线程,而多核处理器可以并行和并发运行线程

同时运行

  • 处理器速度与并发运行:计算机处理器速度极快,单核处理器在微观上以交替方式运行线程,但在宏观上表现为同时运行所有线程

线程 ID

线程ID的定义:每个线程都有唯一的线程ID,但仅在其所属进程上下文中有效

线程ID的数据类型:线程ID使用pthread_t数据类型表示,可通过pthread_self()函数获取

  • #include <pthread.h>
    pthread_t pthread_self(void);

  • 该函数调用总是成功,返回当前线程的线程 ID

线程ID的比较:使用pthread_equal()函数比较两个线程ID是否相等

  • #include <pthread.h>
    int pthread_equal(pthread_t t1, pthread_t t2);

  • 如果两个线程 ID t1 和 t2 相等,则 pthread_equal()返回一个非零值

  • 否则返回 0

  • 作用

    • Linux系统中的pthread_t:在Linux中,pthread_t被定义为无符号长整型

    • 其他系统中的pthread_t:在其他操作系统中,pthread_t可能使用不同的数据类型

    • 可移植性考虑:开发者应将pthread_t视为不透明类型,避免假设其具体类型

    • 线程ID比较:使用pthread_equal()函数比较线程ID,确保跨平台兼容性

作用

  • 线程ID的应用:线程ID在多线程编程中用于标识和操作特定线程,如取消、分离和加入线程等

  • 线程ID的用途:在某些应用中,线程ID可作为动态数据结构的标签,用于标识创建者或后续操作的线程

创建线程

初始线程:程序启动时,创建的进程是单线程的,称为初始线程或主线程

线程创建:主线程通过pthread_create()函数创建新线程,新线程称为子线程

  • #include <pthread.h>
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

    • thread:存储新线程ID的指针

    • attr:线程属性,设为NULL使用默认属性

    • start_routine:新线程执行的函数

    • arg:传递给启动函数的参数,可为NULL

    • 返回值:成功返回0,失败返回错误号

      • 错误处理:pthread_create()返回错误码,不使用全局变量errno,可以把错误的范围限制在引起出错的函数中
  • 线程调度:新线程加入调度队列,执行顺序不确定,如果要指定执行顺序需同步技术确保

主线程休眠:主线程休眠1秒,防止主线程退出导致新线程未运行

编译错误:编译时出现“未定义的引用”错误,因pthread_create不在默认链接库中,需手动指定-lpthread

链接库指定:使用gcc -o testApp testApp.c -lpthread解决链接错误

两个线程的进程ID相同,线程ID不同,Linux下线程ID数值大,类似指针

终止线程

线程终止方式:线程可通过return 语句返回、调用pthread_exit()或pthread_cancel()终止

  • pthread_exit()函数:终止调用线程,参数为线程退出码,可通过pthread_join()获取

    • #include <pthread.h>
      void pthread_exit(void *retval);
  • 如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的

  • 返回值存储:返回值不应在线程栈中分配,以确保线程终止后内容有效

  • pthread_exit()调用:可在任意函数中调用,不同于return,主线程调用后仍允许其他线程运行

    • 主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还可以继续运行

进程终止:任一线程调用exit()、_exit()或_Exit()将导致整个进程终止

回收线程

线程资源回收:父进程用wait()或waitpid()回收子进程,线程用pthread_join()回收

pthread_join()函数:阻塞等待线程终止,获取退出码,回收资源

  • #include <pthread.h>
    int pthread_join(pthread_t thread, void **retval);

    • thread指定等待线程

    • retval存储退出状态

      • 退出状态复制:如果retval非空,pthread_join()复制目标线程的退出状态到*retval

      • 取消状态处理:目标线程被取消时,PTHREAD_CANCELED放入*retval

      • 忽略状态:对线程终止状态不感兴趣时,retval可设为NULL

    • 返回值:成功返回 0;失败将返回错误码

阻塞特性:pthread_join()阻塞等待,线程已终止则立即返回

多线程调用:多个线程同时调用pthread_join()结果不确定

线程分离状态:未分离线程需pthread_join()回收,否则成僵尸线程

僵尸线程影响:僵尸线程浪费资源,过多影响新线程创建

进程终止回收:进程终止时,僵尸线程由父进程回收

  • 所以僵尸线程同样也会被回收

线程与进程差异:线程关系对等,任意线程可调用pthread_join(),进程层次关系固定,父进程唯一能调用wait()

阻塞调用限制:pthread_join()只能阻塞调用,waitpid()可非阻塞调用

取消线程

通常多个线程在进程中并发运行,各执行不同任务,线程可通过调用pthread_exit()或返回语句来退出。在特定编程需求下,可能需要立即终止某线程,这称为取消线程。

  • 例如,当一组线程在执行任务时,若某线程检测到错误,可请求其他线程立即退出,这时取消线程功能便显得非常重要。

取消一个线程

  • 使用 pthread_cancel() 函数可以向指定线程发送取消请求

  • #include <pthread.h>
    int pthread_cancel(pthread_t thread);

    • 参数 thread 用于指定需要取消的线程,成功执行返回 0,失败返回错误码
  • 调用 pthread_cancel() 后,该函数会立即返回,并不会等待目标线程退出

  • 默认情况下,被请求取消的线程将立即执行退出操作,表现类似于调用了带参数 PTHREAD_CANCELED 的 pthread_exit() 函数

  • 线程可以设置自身的取消策略,控制是否以及如何响应取消请求

取消状态以及类型

  • 默认情况下,线程会响应其他线程发送的取消请求并退出,但线程可以通过pthread_setcancelstate() 和 pthread_setcanceltype() 设置自己的取消策略。

  • #include <pthread.h>
    int pthread_setcancelstate(int state, int *oldstate);
    int pthread_setcanceltype(int type, int *oldtype);

  • pthread_setcancelstate() 函数

    • pthread_setcancelstate() 函数将线程的取消状态设置为参数 state 所指定的值,并将旧的取消状态保存在参数 oldstate 指向的缓冲区中

    • 如果对旧状态不感兴趣,可以设置 oldstate 为 NULL;如果调用成功将返回 0,失败则返回非 0 的错误码

    • 参数 state 可以取以下两个值:

      • PTHREAD_CANCEL_ENABLE 表示线程可以被取消,这是新线程的默认取消状态;

      • PTHREAD_CANCEL_DISABLE 表示线程不可被取消,如果该线程接到取消请求,请求会被挂起,直到线程的取消状态变为 PTHREAD_CANCEL_ENABLE

    • pthread_setcancelstate() 函数一次性完成设置取消状态和获取旧状态两个操作,这是一个原子操作

  • pthread_setcanceltype()函数

    • 当线程的取消状态为 PTHREAD_CANCEL_ENABLE 时,取消请求的处理取决于线程的取消类型,可以通过 pthread_setcanceltype() 函数设置

    • pthread_setcanceltype() 函数的参数 type 指定新的取消类型,oldtype 保存旧的取消类型,如果对旧类型不感兴趣,可以将 oldtype 设置为 NULL

    • 函数调用成功返回 0,失败返回非 0 的错误码,设置取消类型和获取旧类型是一个原子操作

    • 参数 type 可以是 PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS

      • PTHREAD_CANCEL_DEFERRED 是默认类型,取消请求会被挂起直到线程到达取消点

      • PTHREAD_CANCEL_ASYNCHRONOUS 类型允许线程在任意时间点被取消,但这种类型应用场景较少

    • 线程调用 fork() 创建子进程时,子进程继承调用线程的取消状态和类型;调用 exec 函数时,新程序的主线程取消状态和类型被重置为默认值 PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED

取消点

  • 当线程的取消类型设置为 PTHREAD_CANCEL_DEFERRED 且处于可取消状态时,取消请求只有在线程到达某个取消点时才会生效

  • 取消点是一系列特定的函数,只有当线程执行到这些函数时,才会响应取消请求

  • 在没有到达取消点之前,取消请求不会被处理,因为系统认为线程正在执行不能被中断的关键代码,此时终止线程可能导致异常

  • 取消点函数包括但不限于以下几种:

  • 可以通过 man 手册进行查询,命令为"man 7 pthreads"

  • 线程在调用标定为取消点的特定函数时,如果收到取消请求,将会执行取消操作,导致线程终止。除了被明确标记为取消点的函数外,其他函数不会触发取消操作,即使在调用这些函数期间线程收到了取消请求。

线程可取消性的检测

  • 如果线程执行的是一个不含取消点的循环(例如for循环、while循环),那么这个线程将不会响应取消请求。除非线程自己主动退出,否则其他线程无法通过发送取消请求来终止它

  • 在实际应用中,可能会遇到线程运行在一个循环中,且循环体内执行的函数不存在任何一个取消点的情况。但如果项目需求需要该线程能通过其他线程发送取消请求来终止,这时可以使用pthread_testcancel()函数

  • pthread_testcancel()函数的功能是创建一个取消点。如果线程有处于挂起状态的取消请求,那么一旦调用该函数,线程就会被终止

    • #include <pthread.h>
      void pthread_testcancel(void);

分离线程

通常情况下,其他线程可以通过调用 pthread_join() 来获取一个线程的返回状态并回收其资源

如果程序员不关心线程的返回状态,只希望线程终止时其资源能自动被回收,可以使用 pthread_detach() 函数对线程进行分离操作

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

    • 参数 thread 指定需要分离的线程

    • 成功调用返回0,失败则返回错误码

  • 使一个指定的线程进入分离状态,这样该线程在终止时会自动回收资源

  • 一个线程可以分离另一个线程,也可以分离自己

    • pthread_detach(pthread_self());

当线程处于分离状态时,就不能再使用 pthread_join() 来获取其终止状态,这个过程是不可逆的。分离状态的线程在终止后会自动释放所有资源

注册线程清理处理函数

与进程中的 atexit()函数类似,线程在终止时可以执行注册过的处理函数,这种处理函数被称为线程清理函数

不同于进程,一个线程可以注册多个清理函数,这些函数被记录在一个栈中。每个线程都可以拥有一个清理函数栈。栈是先进后出的数据结构,所以清理函数的执行顺序与注册顺序相反,当所有清理函数执行完后,线程终止

#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

  • 向清理函数栈中添加或移除清理函数

  • pthread_cleanup_push() 函数用于向清理函数栈中添加一个清理函数

    • 参数 routine 是一个函数指针,指向一个需要添加的清理函数,routine()函数无返回值,只有一个 void *类型参数

    • 参数 arg,当调用清理函数 routine()时,将 arg 作为 routine()函数的参数

  • pthread_cleanup_pop() 函数用于移除栈顶的清理函数

    • 参数 execute如果为0,清理函数不会被调用,只是将清理函数栈中最顶层的函数移除

    • 如果参数 execute 为非0,则会执行并移除栈顶的清理函数

线程在以下三种情况下会执行栈中的清理函数

  • 线程调用 pthread_exit() 退出

  • 线程响应取消请求

  • 用非0参数调用 pthread_cleanup_pop()

虽然 pthread_cleanup_push() 和 pthread_cleanup_pop() 被称为函数,但它们是通过宏实现的,必须在同一作用域中以匹配对的形式使用,否则会编译报错

线程属性

线程栈属性

  • 线程栈空间管理:每个线程有自己的栈空间,栈的起始地址和大小在pthread_attr_t数据结构中定义

  • 函数用于获取和设置栈信息:

    • #include <pthread.h>
      int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
      int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

      • pthread_attr_getstack():获取栈的起始地址和大小

      • pthread_attr_setstack():设置栈的起始地址和大小

      • attr:指向线程属性对象

      • stackaddr:栈起始地址

      • stacksize:栈大小

      • 返回值:成功返回0,失败返回非0错误码

  • 单独获取或设置栈大小和起始地址的函数

    • pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize):设置栈大小

    • pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize):获取栈大小

    • pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr):设置栈起始地址

    • pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr):获取栈起始地址

分离状态属性

  • 线程分离概念:线程分离允许操作系统在线程退出时自动回收其资源,无需其他线程对其进行回收

  • pthread_detach()函数:用于将已创建的线程设置为分离状态

  • 预设线程分离状态:在创建线程时,可以通过修改pthread_attr_t结构中的detachstate属性,预先设置线程为分离状态

  • 设置和获取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);

      • 参数 attr 指向 pthread_attr_t 对象

      • detachstate属性值

        • PTHREAD_CREATE_DETACHED:设置线程为分离状态,结束后资源由操作系统回收,不能被其他线程回收

        • PTHREAD_CREATE_JOINABLE:默认状态,线程可以被其他线程回收,以获取其终止状态信息

    • pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate):设置线程的分离状态

    • pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate):获取线程的分离状态

    • 通过适当设置detachstate属性,可以控制线程的分离或回收机制,优化资源管理

线程安全

在编写多线程应用程序时,必须考虑线程安全,确保程序在多线程环境下正确运行

线程栈

  • 线程栈的独立性:每个线程在进程中都有自己独立的栈地址空间,称为线程栈

  • 线程栈的配置:在创建新线程时,可以配置线程栈的大小和起始地址,但通常使用默认设置

  • 局部变量的独立性:每个线程运行过程中定义的自动变量(局部变量)都分配在自己的线程栈中,不会相互干扰

可重入函数

  • 单线程与多线程的区别:单线程程序只有一条执行流,而多线程程序有多个独立、并发的执行流

  • 信号处理与执行流:信号处理在单线程进程中引入额外的执行流,如主程序和信号处理函数

  • 可重入函数的定义:可重入函数是指当被同一进程的多个执行流同时调用时,每次调用都能产生正确结果的函数

  • 重入的概念:重入是指同一个函数被不同的执行流调用,即使前一个调用还未完成,另一个执行流也开始调用该函数

  • 可重入函数的两种情况:

    • 在含有信号处理的程序中,主程序和信号处理函数可能同时调用同一个函数

    • 在多线程环境下,多个线程可能并发调用同一个函数

  • 不可重入函数的问题:在多线程环境和信号处理相关的应用程序中,不可重入函数可能导致不正确的结果或程序崩溃

  • 可重入函数的分类:

    • 绝对的可重入函数:无论何时调用都能产生预期结果的函数

      • 函数内使用的变量均为局部变量

      • 函数参数和返回值均为值类型

      • 函数内调用的其他函数也是绝对可重入函数

    • 带条件的可重入函数:在满足特定条件时,可以断言该函数是可重入的

      • 需要满足特定条件才能保证可重入性

      • 例如,函数内仅读取全局变量而不修改它,或者传入的指针是其本地变量的地址

  • C库函数的版本:许多C库函数有两个版本,可重入版本和不可重入版本。可重入版本函数名称后面加上“_r”,而不可重入版本没有

  • 可重入函数的标识:通过man手册可以查询函数的“ATTRIBUTES”信息,了解函数是否是线程安全的

    • MT-Safe和MT-Unsafe:

      • MT-Safe:表示多线程安全,函数可以在多线程环境下安全使用

      • MT-Unsafe:表示多线程不安全,函数在多线程环境下使用可能会有问题

    • 带条件的可重入函数:这些函数在MT-Safe标签后面可能带有env或locale等标签,表示它们在满足特定条件时才是可重入的

    • 绝对可重入函数:如果函数是绝对可重入的,MT-Safe标签后面不会带有任何标签。例如,数学库函数sqrt是绝对可重入的

    • 标签的含义:

      • env:表示函数内部会读取进程的某些环境变量,这些变量是全局变量

      • locale:表示函数可能依赖于本地化设置,通常与传入的指针有关

    • 描述信息不一致:有时 ATTRIBUTES 描述信息与非线程安全函数列表不一致

    • 处理原则:应以非线程安全函数列表为准,默认该函数是线程安全的

线程安全函数

  • 线程安全函数与可重入函数的区别

    • 线程安全函数:可以被多个线程安全地调用的函数。线程安全函数不一定是可重入的,因为它们可能通过锁或其他同步机制来保护共享资源,从而避免并发访问中的问题

    • 可重入函数:可重入函数是线程安全函数的一个子集。一个可重入函数可以在任何时刻被中断并由另一个执行流(线程或信号处理函数)安全地调用,不依赖共享资源或只以线程安全的方式访问共享资源

  • 线程安全函数的实现

    • 要使一个函数线程安全,通常需要采用同步机制(如互斥锁)来保护对共享资源的访问。例如,对于修改全局变量的函数,如果在访问全局变量时加锁,使得一次只有一个线程能修改该变量,然后在修改完成后解锁,这样的函数就成为线程安全的。但是,使用锁会引入其他问题,如死锁或性能下降
  • POSIX标准和线程安全

    • POSIX标准要求大多数库函数必须是线程安全的。然而,仍有一些例外,这些函数通常会在文档中明确标记为线程不安全的。

    • POSIX.1-2001 和 POSIX.1-2008 中列出的线程不安全函数

  • 如何确认函数的线程安全性

    • 通过man页面的ATTRIBUTES部分查找函数的线程安性。如果标记为MT-Safe,则表示函数是线程安全的;如果标记为MT-Unsafe,则表示函数不是线程安全的

    • 注意,即使函数是线程安全的,也可能需要在特定的上下文或条件下才能保证其安全性

  • 多线程编程的挑战

    • 不仅要了解哪些函数是线程安全的,还要理解线程间如何共享资源,以及如何使用同步机制(如互斥锁、信号量等)来避免竞争条件和数据不一致

一次性初始化

  • 在多线程编程中,确保某些初始化代码只执行一次是一个常见的需求。pthread_once()函数提供了一种机制来实现这一点,无论有多少线程尝试调用它,指定的初始化函数只会被执行一次

  • #include <pthread.h>
    pthread_once_t once_control = PTHREAD_ONCE_INIT;
    int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

    • once_control: 这是一个pthread_once_t类型的指针,用于控制初始化函数的执行。在调用pthread_once()之前,需要定义一个pthread_once_t类型的静态变量,并使用PTHREAD_ONCE_INIT宏进行初始化

      • 如 果 参 数 once_control 指向的 pthread_once_t 类 型 变 量 , 其 初 值 不 是 PTHREAD_ONCE_INIT ,
        pthread_once()的行为将是不正常的;PTHREAD_ONCE_INIT 宏在<pthread.h>头文件中定义
    • init_routine: 这是一个函数指针,指向需要只执行一次的初始化函数

    • 返回值:调用成功返回 0;失败则返回错误编码以指示错误原因

      • 当调用 pthread_once 成功返回时,调用总是能够肯定所有的状态已经初始化完成了

线程特有数据

  • 线程特有数据(Thread-Specific Data)是一种机制,允许每个线程维护自己的变量副本,从而避免多个线程之间的数据共享问题。这在处理非线程安全函数时特别有用,可以将这些函数转换为线程安全函数

  • 线程特有数据的核心函数

    • pthread_key_create(): 创建一个特有数据键

      • #include <pthread.h>
        int pthread_key_create(pthread_key_t *key, void (destructor)(void));

        • key:这是一个 pthread_key_t 类型的指针。调用 pthread_key_create() 函数之前,需要定义一个 pthread_key_t 类型的变量,调用时 key 参数指向这个变量。函数成功执行后,这个变量将包含新创建的特有数据键

        • destructor:这是一个函数指针,指向一个自定义的解构函数。这个函数会在使用线程特有数据的线程终止时被自动调用,用于释放与特有数据键关联的线程私有数据区占用的内存空间

          • void destructor(void value)
            {
            /
            code */
            }
        • 返回值:
          成功时返回 0。
          失败时返回一个错误编号,这个错误编号其实就是全局变量 errno,可以使用诸如 strerror() 函数查看其错误字符串信息。

    • pthread_setspecific(): 将线程私有数据缓冲区与特有数据键关联

      • #include <pthread.h>
        int pthread_setspecific(pthread_key_t key, const void *value);

        • key:这是一个 pthread_key_t 类型的变量,应赋值为调用 pthread_key_create() 函数时创建的特有数据键。也就是 pthread_key_create() 函数的参数 key 所指向的 pthread_key_t 变量

        • value:这是一个 void 类型的指针,指向由调用者分配的一块内存,作为线程的私有数据缓冲区。当线程终止时,会自动调用参数 key 指定的特有数据键对应的解构函数来释放这一块动态申请的内存空间

        • 返回值:调用成功返回 0;失败将返回一个错误编码,可以使用诸如 strerror()函数查看其错误字符串信息

    • pthread_getspecific(): 获取线程私有数据缓冲区

      • #include <pthread.h>
        void *pthread_getspecific(pthread_key_t key);

        • key: 应赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数的参数 key 指向的 pthread_key_t 变量

        • 返回值:返回一个指针,指向该缓冲区

      • 未设置情况:如果当前线程未设置私有数据缓冲区与特有数据键关联,则返回 NULL

      • 初次调用判断:可以利用返回值是否为 NULL 来判断当前线程是否为初次调用该函数

      • 初次调用处理:如果是初次调用,需要为该线程分配私有数据缓冲区

    • pthread_key_delete():删除一个特有数据键(key)

      • #include <pthread.h>
        int pthread_key_delete(pthread_key_t key);

        • key :要删除的键

        • 返回值:成功返回 0,失败将返回一个错误编号

      • 函数功能:pthread_key_delete() 函数释放指定的特有数据键,供后续 pthread_key_create() 调用使用

      • 不触发解构函数:调用 pthread_key_delete() 时,不会检查或触发键的解构函数,也不会释放关联的线程私有数据区内存

      • 终止时不再执行解构函数:调用 pthread_key_delete() 后,线程终止时不再执行键的解构函数

      • 调用前的条件:在调用 pthread_key_delete() 之前,必须确保所有线程已释放私有数据区,并且该键不再使用

      • 未定义行为:调用 pthread_key_delete() 后,任何使用该键的操作(如 pthread_setspecific() 或 pthread_getspecific())都会导致未定义行为

线程局部存储

  • 全局变量与线程局部存储

    • 全局变量在进程中所有线程共享

    • 使用 __thread 修饰符定义的变量,每个线程拥有其独立副本

  • 线程局部存储的优点

    • 比线程特有数据更简单

    • 只需在变量声明中加入 __thread 修饰符

  • 声明和使用注意事项

    • __thread 关键字需紧随 static 或 extern 之后

    • 可设置初始值

    • 可使用取值操作符(&)获取地址

更多细节问题-线程与信号

线程与信号

  • 线程技术需要兼容现有的信号和进程控制

  • 信号模型基于进程设计,出现时间早于线程,导致一些冲突

  • 信号在单线程和多线程环境中都要保持功能

  • 信号与线程结合使用的复杂性

    • 信号有时属于进程层面,有时属于线程层面

    • 信号处理函数和系统默认行为在进程层面共享

    • 某些信号(如硬件异常信号、SIGPIPE、pthread_kill()发送的信号)针对特定线程

线程的信号掩码

  • 针对线程而非整个进程

  • 每个线程可独立设置其信号掩码

向线程发送信号

  • kill()和sigqueue()针对整个进程,pthread_kill()和pthread_sigqueue()可针对特定线程

  • #include <signal.h>
    int pthread_kill(pthread_t thread, int sig);

    • 参数thread表示线程ID,用于指定进程中的目标线程

    • 参数sig是要发送的信号。如果sig为0,则不发送信号,但会执行错误检查

    • 如果函数调用成功,返回0;如果失败,返回错误编号,不会发送信号

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

    • 参数thread是线程ID,用于指定目标线程。目标线程与调用pthread_sigqueue()的线程必须属于同一个进程

    • 参数sig用于指定要发送的信号,参数value用于指定伴随数据,这与sigqueue()函数中的value参数意义相同

异步信号安全函数

  • 指的是可以在信号处理函数中可以被安全调用的线程安全函数

    • 比线程安全函数要求更高
  • 异步信号安全函数是可重入函数,但线程安全函数未必是异步信号安全函数

  • 信号处理函数的安全问题主要由两个原因造成

    • 信号是异步的,可能在任何时间点中断主程序

    • 信号处理函数与线程执行流存在区别,它们在同一个线程中执行

  • 可重入函数的要求最严格,通常可重入函数一定是线程安全和异步信号安全的

  • 为了使线程安全函数func()成为异步信号安全函数,可以在获取锁之前设置信号掩码,禁止在锁期间接收特定信号,从而避免信号中断函数执行

  • 异步信号安全函数,可以通过 man 手册查询,执行命令"man 7 signal"

  • 一个安全的信号处理函数需要满足以下条件

    • 信号处理函数本身必须是可重入的,并且只能调用异步信号安全函数

    • 当主程序执行不安全函数或操作可能被信号处理函数更新的全局数据结构时,需要阻塞信号的传递,以避免信号中断导致的不安全行为

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1926708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LabVIEW红外热波图像缺陷检

开发使用LabVIEW开发的红外热波图像缺陷检测系统。该系统结合红外热像仪、工业相机和高效的数据采集硬件&#xff0c;实现对工件表面缺陷的自动检测和分析。通过LabVIEW的强大功能&#xff0c;系统能够实时采集、处理和显示红外热波图像&#xff0c;有效提高了检测的精度和效率…

时域分析----移动平均滤波器介绍及其在金融应用示例

介绍 移动平均滤波器&#xff08;Moving Average Filter&#xff09;是一种基本但功能强大的信号处理技术&#xff0c;广泛应用于各种数据平滑和去噪任务中。其主要目的是通过对数据进行平均处理&#xff0c;减少随机波动和噪声&#xff0c;从而突出数据中的趋势和规律。移动平…

Win11任务栏当中对 STM32CubeMX 的堆叠问题

当打开多个 CubeMX 程序的时候&#xff0c;Win11 自动将其进行了堆叠&#xff0c;这时候就无法进行预览与打开。 问题分析&#xff1a;大部分ST的工具都是基于 JDK 来进行开发的&#xff0c;Win11 将其识别成了同一个 Binary 但是实际上他们并不是同一个&#xff0c;通过配置…

数据治理项目中,数据运营团队如何搭建能提升数据应用效果?

引言&#xff1a;在数据治理项目中&#xff0c;数据运营团队的搭建对于提升数据应用效果具有关键作用。以下是一些具体的步骤和策略&#xff0c;用于构建高效的数据运营团队以优化数据应用效果&#xff1a; 一、明确团队目标和职责 确定数据应用目标&#xff1a;首先&#xf…

【接口自动化_06课_Pytest+Excel+Allure完整框架集成】

一、logging在接口自动化里的应用 1、设置日志的配置&#xff0c;并收集日志文件 日志的设置需要在pytest.ini文件里设置。这个里面尽量不要有中文 2、debug日志的打印 pytest.ini文件的开关一定得是true才能在控制台打印日志 import allure import pytest from P06_PytestFr…

JavaScript(9) ----this指向问题,bind,call,apply等方法

目录 this指向问题 全局函数调用&#xff1a; 对象方法调用&#xff1a; 构造函数调用&#xff1a; 事件处理&#xff1a; 箭头函数&#xff1a; setTimeout和setInterval 7.使用call、apply或bind call 方法 apply 方法 bind 方法 总结 this指向问题 全局函数调用…

基于conda包的环境创建、激活、管理与删除

Anaconda是一个免费、易于安装的包管理器、环境管理器和 Python 发行版&#xff0c;支持平台包括Windows、macOS 和 Linux。下载安装地址&#xff1a;Download Anaconda Distribution | Anaconda 很多不同的项目可能需要使用不同的环境。例如某个项目需要使用pytorch1.6&#x…

STM32MP135裸机编程:支持内存非对齐访问

0 前言 使用stm32官方可视化初始化代码生成工具STM32CubeMX生成的工程GCC编译选项默认不支持非对齐访问&#xff0c;在我们进行非对齐的访问时就会进入数据异常中断DAbt中。为了解决这一问题&#xff0c;我们需要在GCC编译选项中加上一处配置。 1 操作方法 右键STM32CubeIDE…

ArkTS基础快速入门

初识ArkTS语言 ArkTS&#xff1a;是一门用于开发鸿蒙应用的编程语言。 ArkTS提供了声明式UI范式、状态管理支持等相应的能力&#xff0c;让开发者可以以更简洁、更自然的方式开发应用。 同时&#xff0c;它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风格的基础…

启动yarn后,其他节点没有NodeManager

写在前面&#xff1a; 这个问题虽然折磨了我两天&#xff0c;但是原因特别蠢&#xff0c;可能与各位不一定一样&#xff0c;我是因为ResourceManager的节点的"/etc/hadoop/workers"文件没有配置好&#xff08;没有配hadoop102和hadoop104&#xff09;&#xff0c;但排…

FPGA学习笔记(一) FPGA最小系统

文章目录 前言一、FPGA最小系统总结 前言 今天学习下FPGA的最小系统一、FPGA最小系统 FPGA最小系统与STM32最小系统类似&#xff0c;由供电电源&#xff0c;时钟电路晶振&#xff0c;复位和调试接口JTAG以及FLASH配置芯片组成&#xff0c;其与STM32最大的不同之处就是必须要有…

QQ频道导航退出

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/140413538 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…

three.js添加多个画布

多个场景的渲染需要多个画布&#xff0c;但是这样会导致加载模型纹理次数变多&#xff0c;基本方法是用一张canvas在整个背景中填充视口&#xff0c;利用其它元素作为每个虚拟画布&#xff0c;只在canvas中加载一个渲染器renderer&#xff0c;并为每个虚拟画布创建一个场景&…

实训第一天笔记

#初始化一个新的NPM项目(根据提示操作) npm init #安装TSC、TSLint和NodeJS的类型声明 npm install -s typescript tslint types/node 在根目录中新建一个名为tsconfig.json的文件&#xff0c;然后在代码编辑器中打开&#xff0c;写入下述内容&#xff1a; {"compilerO…

Chromium CI/CD 之Jenkins实用指南2024-如何创建新节点(三)

1. 前言 在前一篇《Jenkins实用指南2024-系统基本配置&#xff08;二&#xff09;》中&#xff0c;我们详细介绍了如何对Jenkins进行基本配置&#xff0c;包括系统设置、安全配置、插件管理以及创建第一个Job。通过这些配置&#xff0c;您的Jenkins环境已经具备了基本的功能和…

Qt文件下载工具

在Qt中实现文件下载功能&#xff0c;通常可以通过多种方式来完成&#xff0c;包括使用 QNetworkAccessManager 和 QNetworkReply 类&#xff0c;或者使用更高级别的 QHttpMultiPart 类。以下是两种常见的实现方法&#xff1a; 方法1&#xff1a;使用 QNetworkAccessManager 和…

AI操作系统势头正猛,以后LINUX,和window,Android,IOS等等的OS都将被AI OS所取代!

AI操作系统是一种旨在利用人工智能技术来优化和管理计算资源的操作系统。它不仅仅是一个传统意义上的操作系统&#xff0c;而是一个能够自主学习和适应用户需求的智能平台。以下是对AI操作系统的一些关键特点和它是否能取代现有操作系统的讨论&#xff1a; AI操作系统的关键特…

OpenCV图像处理——判断轮廓是否在圆环内

要判断一个轮廓是否在圆环内&#xff0c;可以将问题分解为两个步骤&#xff1a; 确保轮廓的所有点都在外圆内。确保轮廓的所有点都在内圆外。 下面是一个完整的示例代码&#xff0c;展示如何实现这一点&#xff1a; #include <opencv2/opencv.hpp> #include <iostr…

QT VTK 简单测试工程

目录 1 目录结构 2 文件源码 3 运行结果 4 报错及处理 使用编译好的VTK库进行测试 1 目录结构 2 文件源码 Pro文件 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c17# You can make your code fail to compile if it uses deprecated APIs. #…

Iceberg概念和特性

1. 快照 Iceberg会随着时间的推进,跟踪表生命周期中的所有数据集变化,并使用快照(Snapshots)来表示每一次变化后的数据集合,每一次数据操作的事务提交均会产生一个快照,并将其记录在元数据文件(Metadata)中。 基于快照的概念,Iceberg有以下特性: 事务性:写入快照成…