课程内容#
进程间通信复杂 [内存空间独立]、切换成本高 [时间局部性],所以发明了线程
线程#
一个进程的分支 [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] 如果一个线程导致内存崩溃,极有可能也产生同归于尽的效果,即进程中的所有线程都死亡
- ① 自杀:自己调用 pthread_exit
- 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_exit 里的 retval 是一个指针,按照惯例,这里就要用二级指针了 [同理,如果接收的是一个 int 数据,这里就用 int *]
- 进一步原因:为了可以修改传过来的指针
- 这里有一个博客也提到了:探讨 pthread_join () 函数第二个形参为啥是二级指针问题——CSDN
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):等待任务加入
-
- 任务队列 pop:弹出任务,供线程执行
-
- 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#
- 创建的一个进程,又代表一个主线程
- 多线程在单 cpu 中还是顺序执行的,所以在单核 CPU 上,不推荐使用多线程
- 参考对于多线程程序,单核 cpu 与多核 cpu 是怎么工作的——cnblogs