线程
线程概念
简介
-
线程定义:线程是操作系统进行调度的最小单位,包含在进程内,是进程中的实际执行单元
-
线程特性:一个线程代表进程中的一个单一顺序控制流,即执行路径
-
多线程应用:一个进程可以包含多个线程,这些线程可以并发执行,每个线程处理不同的任务
-
实际应用示例:例如,一个应用程序需要同时运行两个任务(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);
- #include <pthread.h>
-
如果线程是在 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);
- #include <pthread.h>
分离线程
通常情况下,其他线程可以通过调用 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>头文件中定义
- 如 果 参 数 once_control 指向的 pthread_once_t 类 型 变 量 , 其 初 值 不 是 PTHREAD_ONCE_INIT ,
-
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 */
}
- void destructor(void value)
-
返回值:
成功时返回 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"
-
一个安全的信号处理函数需要满足以下条件
-
信号处理函数本身必须是可重入的,并且只能调用异步信号安全函数
-
当主程序执行不安全函数或操作可能被信号处理函数更新的全局数据结构时,需要阻塞信号的传递,以避免信号中断导致的不安全行为
-