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的设置
    • 键值与某个地址对应
  • 线程取消: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返回之前,在子进程上下文中执行,释放第一步中获取的所有锁
        • 注意:子进程中的锁和父进程中的锁不是同一把锁?!!!
  • 线程和I/O

    • pread
    • pwrite
    • 这两个函数对偏移量的设定和数据的读写操作是原子的

APUE-Learning-线程
http://example.com/2024/07/22/APUE-Learning-线程/
作者
Cyokeo
发布于
2024年7月22日
许可协议