Bo2SS

Bo2SS

6 多线程编程基础

课程内容#

进程间通信复杂 [内存空间独立]、切换成本高 [时间局部性],所以发明了线程

线程#

一个进程的分支 [pthread],本质上是一个轻量级的进程

  • 通信方便,因为一个进程里的多个线程共享内存
  • 切换成本低,因为内存是共享的,在切换线程时不需要置换缓存

pthread_creat#

创建一个新的线程

  • man pthread_create
  • 原型
    • 图片
    • thread:线程 id [注意:不是数字类型,不能直接用 == 判断,见 pthread_equal]
    • attr:属性
    • arg 是 start_routine 的参数
  • 描述
    • 图片
    • 开启一个新的线程,并执行 start_route 函数
      • start_route 函数只能收一个 arg 参数 [多个参数可用结构体封装]
    • 线程有4 种终结的方式 [作为工具人,死亡方式很重要]
      • ① 自杀:自己调用 pthread_exit
        • 同一进程下的线程可使用 pthread_join 接收它的死亡状态 [有点像 wait]
      • ② 正常死亡:从 start_routine 函数中返回
        • 与 pthread_exit 方式等价
      • ③ 他杀:pthread_cancel
      • ④ 同归于尽:进程中的一个线程调用 exit,或者主线程从 main 函数中返回
        • [PS] 如果一个线程导致内存崩溃,极有可能也产生同归于尽的效果,即进程中的所有线程都死亡
    • attr 可以为 NULL,对应默认属性
    • 调用成功后,会将线程 id 保存在 thread 变量中,后续都可以通过这个 id 使用它 [类似文件描述符]
  • 返回值
    • 图片
    • 0,成功;否则,失败

——3 种终止线程的方法 ——#

pthread_exit#

线程自杀

  • man pthread_exit
  • 图片
  • ❗ 线程自杀,将 retval 传给 join 的线程 [线程默认是可加入的]
  • 执行完 pthread_cleanup_push 注册的函数,随后释放线程特有的数据
    • 进程中共享的资源不会被释放 [因为还有兄弟线程]
    • atexit 注册的函数不会被调用 [这是属于进程的]
  • 在最后一个线程结束后,进程以 exit (0) 的方式结束,释放进程共享的资源,并执行 atexit 注册的函数
  • 【注意】线程和进程的关系

pthread_cancel#

给一个线程发送取消请求 [他杀]

  • man pthread_cancel
  • 图片
  • 线程取消的可能性和时间取决于两个属性:state、type
  • state
    • 可杀 [默认]
    • 不可杀:此时收到的取消命令会排在队列中
  • type
    • 推迟的 [默认]:等到线程的下一次调用
    • 异步的:立即,但系统不能保证

exit [进程相关]#

终止普通进程

  • man exit
  • 图片
  • 传给父进程的值为:status & 0377
    • 注意:0377 是 8 进制,对应二进制的 8 个 1,也就是只保留 status 的低 8 位
  • 使用 atexit 和 on_exit 注册的函数会以与注册相反的顺序被调用
    • 可以套娃:注册的函数中还可以有注册,并且会放在调用列表的最前面
    • 如果注册的函数没有返回,比如调用了_exit 或者使用信号自杀,剩余的函数不会再被调用,exit 相关的处理也会被禁止
    • 多次注册函数,会被多次调用
  • ⭐ exit 后会刷新并关闭所有标准 IO 流

—— 监控线程状态相关 ——#

pthread_join#

等待线程终止

  • man pthread_join
  • 图片
  • 可类比进程中的 wait 函数
  • retval 接收线程退出状态
    • 如果线程是自杀的,则复制 pthread_exit 中的 retval 值
    • 如果线程是他杀的,则赋值为 PTHREAD_CANCELED
  • [思考] 这里的 retval 是二级指针,也就是指针的指针,为什么?

pthread_detach#

分离线程

  • man pthread_detach
  • 图片
  • 线程被分离后,其终止时系统会自动回收其资源,而不需要其他线程阻塞等待它终止了
  • 一般与pthread_self配合使自己分离
    • pthread_self:获得调用线程 [自己] 的 id
  • 参考pthread_join () 与 pthread_detach () 详解——CSDN

—— 附加 ——#

pthread_yield#

让出处理器 [processor]

  • man pthread_yield
  • 图片
  • [类似 sleep 的效果]
  • 该方法只用于某些系统,更标准的用法:sched_yield
    • 对于协同式系统,调用此函数来主动让出 CPU
    • 对于抢占式系统,内核会进行调度,该函数没有太大意义,也可直接使用 sleep
    • 协同式与抢占式,详见 4 高级进程管理 ——调度器分类

pthread_equal#

对比两个线程的 id [不能直接用 == 判等]

  • man pthread_equal
  • 图片
  • 如果相等,返回一个非零值

线程池#

让一堆线程在池子里待命,随时工作

基本组成部分👇

任务队列:存放需要处理的任务

  • [循环队列更佳]
  • 基本操作:init、push、pop

② 多个线程:时刻准备着,减少创建响应和销毁的时间

③ 线程功能:do_work ()

  • while (1):等待任务加入
      1. 任务队列 pop:弹出任务,供线程执行
      1. do-work ():在CPU中执行任务

❗ 注意:push 和 pop 的时候都需要加锁,防止数据竞争 [饥渴的线程]

  • 详见代码演示

内核线程、用户线程#

线程是谁产生的?线程模型

  • 两者的区别主要在调度上:内核线程由内核调度;用户线程由用户进程调度
  • 内核线程的优势
    • ① 每个内核线程有自己的时间片。所以其进程因为有多个线程,会拥有更多的处理器时间;而用户进程不会因为多分出了几个用户线程获得更多的处理器时间
    • ② 如果一个内核线程被阻塞,进程中剩余的线程还可以继续运行。如果一个用户线程被阻塞,整个进程就都会被阻塞
      • PS:如果一个内核线程给自己的进程发送 sleep 信号,这个线程依然可以继续运行
  • 用户线程的优势
    • ① 切换成本低。不会涉及用户态向内核态的转换
    • ② 调度算法完全由进程控制。用户进程可以用自己的调度算法,所以自主性更好;而内核线程的调度,对用户来说是一个黑箱
  • 所以可以结合两者的优势,设计既有内核线程,也有用户线程的混合式线程

代码演示#

简单使用多线程#

  • 图片
  • 图片
  • 关注:pthread_create 函数的使用
  • 如果在创建线程后,没有 usleep 或者 usleep 的时间过短,可能发生:
    • 主线程 return,从而使所有子线程同归于尽
    • 此时会发生某些输出出现两次的情况,如①、② [③为正常输出]
    • 图片
    • 猜测:输出缓冲区的问题,在线程突然结束时,又输出了一次缓冲区的内容 [缓冲区没来得及更新]
    • [PS] 使用 fflush 也不能解决,可能因为线程是突然结束的
  • 注意:所有线程都可以操作同一个地址的值

线程池#

thread_pool.h

  • 图片
  • 任务队列的定义,及基本操作

thread_pool.c

  • 图片
  • 图片
  • 注意:加解锁、发信号;判断队列满 / 空;判断指针是否到达队尾

1.test.c

  • 图片
  • 图片
  • 存储数据的 buff 为二维数组,可避免线程读取数据时,主线程通过地址改变了数据
  • usleep 的妙用:避免 while (1) 循环太耗 CPU
  • pthread_detach 一般与 pthread_self 配合使用
  • fgets:读取文件到缓冲区
    • man fgets
    • 图片
    • 图片
    • 按行读
  • 输出效果
    • 图片
    • 基本输出:push [任务队列输出];pop [任务队列输出] + do_work [线程输出]

附加知识点#

  • 编译包含线程相关函数的文件时,注意使用 - lpthread

Tips#


加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。