APUE-Learning-线程
线程
每个线程都包含有表示执行环境所必须的信息
- 进程中标识线程的线程ID。pthread_t,所有可移植的操作系统实现不能把它作为整数处理,因此必须提供一个函数来对两个线程ID进行比较
- 一组寄存器值
- 栈
- 调度优先级和调度策略
- 信号屏蔽字 -> 继承于创建它的线程
- errno变量
- 线程私有数据
POSIX线程的功能测试宏是**_POSIX_THREADS**,可以用它在编译时确定是否支持posix线程
线程终止
- 影响整个进程
- 如果进程中的任意线程调用了exit, _Exit, _exit,那么整个进程就会终止
- 如果信号的默认行为是终止进程,那么发送到线程的信号就会终止整个进程
- 单个线程退出
- 从线程执行函数中退出
- 被同一进程中的其他线程终止:pthread_cancel(),仅提出终止线程的请求,不等待线程终止
- 线程调用pthread_exit()
- pthread_join() -> 传参时遇到指针一定注意:内存应该处在栈上?还是堆上?所有者是哪个线程?
- 调用线程一直阻塞,直到指定的线程调用pthread_exit()、从执行函数中返回、或者被其他线程取消
- 影响整个进程
线程的分离状态
- 已被分离的线程在终止时,其底层存储资源可以立即被收回
- 线程被分离后不能调用pthread_join()等待它终止,会产生未定义行为
- 可以调用pthread_detach()分离线程
- 在pthread_create时控制传入的线程属性参数,可以直接创建已处于分离状态的线程
线程同步
- 程序使用变量的方式也会引起竞争,导致不一致的情况发生。例如,对某个变量+1,然后基于这个值做出某种决定。因为增量操作步骤和这个决定步骤的组合不是原子操作,所以就给不一致情况的出现提供了可能
互斥量 -> pthread_mutex_t
pthread_mutex_lock -> 可能会阻塞
pthread_mutex_trylock -> 不会阻塞;不能加锁时返回EBUSY
如果有多个线程阻塞在获取锁的状态;当锁被释放时,所有线程都被唤醒?,最终只有一个线程能获取锁,其他线程仍进入阻塞状态?
- 这种“惊群现象”会发生嘛?即线程何时被唤醒?
- 如果等待的线程数>1,内核可以只唤醒一个线程,避免惊群 <- 这个是futex2(fast user mutex)【Documentation\translations\zh_CN\userspace-api\futex2.rst】
- 翻看pthread_mutex_unlock时,传给futex_wake的参数中nr = 1,即只唤醒一个等待的线程
- 翻看Linux-futex系统调用的实现,看到其先遍历futex的等待队列,把待唤醒的线程结构加入wake_q【超过nr后,不再添加】,之后调用
wake_up_q对wake_q里面的线程进行统一的唤醒:-> 线程使用task_struct结构表示,其中有个成员struct wake_q_node wake_q;,辅助进行这些步骤
- 因此mutex不会产生惊群现象
- 值得注意的是:futex簇有很多操作:wait,wake等,但都使用同一个系统调用号,最终在
do_futex()统一处理,根据传参提取出(cmd)信息,进行细致的处理
可以仔细控制相关互斥量的加锁顺序以避免死锁:
- 程序1: A -> B;程序2:B -> A;这种情况下可能会出现1获得A,2获得B,之后A阻塞于获取B,而B阻塞与获取A
- 如果所有程序都以A -> B的顺序加锁,那么可以避免相关的死锁
当锁比较多,不好仔细控制加锁顺序时,可以采用解除已获取锁,并进入等待,稍后再试的方式来避免死锁
- 即使用trylock,如果返回EBUSY,则释放之前已获取的锁,并挂起等待
读写锁 -> pthread_rwlock_t
- 三种状态:适合于读次数远大于写次数的情况
- 读加锁:后续读加锁的线程可以获得访问;而写加锁的线程被阻塞
- 写加锁:后续任意形式的加锁都会被阻塞
- 不加锁:->
条件变量 -> pthread_cond_t
- 需要配合互斥量一起使用,条件本身由互斥量保护,线程在改变条件状态之前必须首先锁住互斥量
- 调用pthread_cond_wait时,要把已经锁住的互斥量传递给该函数。一般在while条件循环判断条件是否成立,在while循环体里面调用cond_wiat()
- 两个唤醒操作
- pthread_cond_signal(),唤醒一个特定的线程
- pthread_cond_broadcast(),唤醒所有等待线程
自旋锁 -> pthread_spinlock_t
- 不通过挂起使进程阻塞,而是循环判断
- 适用于锁持有时间短,短至自旋开销远小于线程切换、重新调度的时间开销
- 当前只在极少数情况下适合使用
屏障 -> pthread_barrier_t
- 允许每个线程等待,知道所有合作的线程都到达某一点,然后从该点继续执行。pthread_join()就是一种屏障
- 参数count指定必须到达屏障的线程数目
- 线程主动调用pthread_barrier_wait,表明该线程已完成工作,等待其它线程完成
线程控制
当互斥量/读写锁/条件变量/屏障位于进程共享内存区域,则该互斥量/读写锁/条件变量/屏障可以用于进程间同步
- 需要设置属性为进程间共享
如果一个函数对多个线程来说是可重入的,就说这个线程是线程安全的
同一进程的多个线程如何保持数据的私有性?
- 提供了让基于进程的接口适应多线程环境的机制 -> errno
- 一个线程在系统调用中重置errno,也不会影响其它线程对errno的设置
- 键值与某个地址对应
- 提供了让基于进程的接口适应多线程环境的机制 -> errno
线程取消:pthread_cancel()
- 调用pthread_cancel()并不会阻塞等待线程结束
- 调用后,目标线程可能还处于运行状态,只有在到达取消点时,它才会检查是否被设置了取消状态 -> 推迟取消模式
- 如果被设置了取消状态,则结束自身的运行
- 系统中有很多的cancel point
- 如果该线程取消属性被设置为:PTHREAD_CANCEL_DISABLE,
- 所有对该线程的取消请求会被保留,但处于挂起状态,即在线程到达取消点时,并不会检查取消状态
- 当该线程被再次设置为PTHREAD_CANCEL_ENABLE,在到达取消点时,会检查取消状态【之前设置的取消状态还是有效的】
- 可以主动调用pthread_testcancel(),由开发人员手动加入取消点
- 异步取消模式:当线程的取消为该种类型时,线程可以在任意时间被取消
线程和信号
- 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的
- 进程中的信号是递送到单个线程的
- pthread_sigmask()用于设置线程的信号屏蔽字
- sigwait(),线程调用等待一个或多个信号的出现。返回之前,sigwait将从进程中移除处于挂起等待状态的信号
- 多个线程在sigwait()调用中因同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可从sigwait()返回
- 如果一个信号被设置了sigaction()[即被捕获],这时由操作系统决定是让sigwait返回,还是激活信号处理程序:只能2选1
- 使用pthread_kill()将1个信号发送给线程
- 为了避免错误行为,线程在调用sigwait()之前,必须阻塞那些它需要等待的信号。该函数内部会原子地取消即将等待信号集的阻塞状态
线程和fork()
- 线程调用fork()创建子进程,从父进程继承了每个互斥量、读写锁和条件变量的状态;父进程有多个线程的情况下,子进程中只有一个线程,它是由父进程中调用fork的线程的副本。如果子进程从fork返回以后,不是紧接着调用exec的话,就需要清理锁状态
- 但是子进程不知道它占有了哪些锁【这些占有的锁可能是父进程中的其它线程持有的】,因此它无法做细致的清理
- 如果fork返回后,紧接着调用exec,就可以避免上述的情况,因为旧的地址空间会被丢弃,所以锁的状态无关紧要
- 但如果子进程还要继续进行部分处理工作,就需要重点关注:
- pthread_atfork,在三个阶段进行辅助清理锁资源
- 父进程调用fork创建子进程之前调用,获取父进程定义的所有锁
- fork创建子进程以后、返回之前在父进程上下文中执行,对前一步获取的所有锁进行解锁
- fork返回之前,在子进程上下文中执行,释放第一步中获取的所有锁
- 注意:子进程中的锁和父进程中的锁不是同一把锁?!!!
- pthread_atfork,在三个阶段进行辅助清理锁资源
线程和I/O
- pread
- pwrite
- 这两个函数对偏移量的设定和数据的读写操作是原子的
APUE-Learning-线程
http://example.com/2024/07/22/APUE-Learning-线程/