1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
pthread 多线程机制.md 12.13 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-03-15 01:43 . 背景知识

pthread 多线程机制

前言

多线程模型和异步模型是有差别的,异步模型一般是语言层面提供的 async/await 之类的语言机制,它基于协程(一个可多次重入的函数)的概念。多线程模型不是语言层面的,而是操作系统提供的机制。多线程才能有效利用 cpu 多核,实现所谓的并行执行。

因此,不管语言是怎么封装多线程,它归根结底就是操作系统底层的机制。pthread 是 linux 系统的实现。

多线程下的程序困境

单线程下的程序是原子性,可以将一段程序理解为必然的结果,但是在多线程下却不一样。多线程对某个数据同时操作是,读写顺序是随机的(甚至在指令级也是非原子性的),这就让我们编写程序一直以来依赖的假设不成立,如果我们不确定顺序,怎么能书写正确的逻辑?这是不可能做到的。所以归根结底,多线程的编程方式:

  1. 分割任务,使任务之间没有重叠,最后等待各个部分完成
  2. 重叠部分,串行化。也就是实际上让多线程转化为单线程,从而有序化,可编程化

也就是说多线程编程并不神奇,也没有量子化,它的重点是在隔离分割,让任务和任务之间实际并没有什么干涉。而对于确实存在干涉的局部,进行单线程化,从而将多线程的不确定性消除。

也就是归根结底,是如何提高任务之间的并行性,让任务之间最大的隔离开来,如果发生干涉,必然会失去多线程,那么就算你能正确的书写同步代码,又有什么意义?(指无法提高并行度)所以要提高并行度,就要理解哪些任务是可以并行的。比如:

  1. 线程独立的变量:栈,线程局部变量,常量。反例就是:全局变量。比较暧昧的是函数参数,对象等。
  2. 事务角度宏观划分哪些任务可同时执行,哪些任务必须顺序执行。

背景知识

  1. 互斥算法:peterson 算法描述(不基于原子操作):
    1. 有 A 和 B 两人上厕所
    2. A 举旗 a,然后写厕所门标志 b ,当 B 没有举旗或标志为 a,进入厕所
    3. 同时,B 举旗 b,写标志 a(覆盖),当 A 没有举旗或标志为 b,进入厕所
    4. 否则等待
    5. 出厕所时把旗子放下
  2. 互斥算法满足:
    1. 互斥访问:永远只有一个人在厕所
    2. 空闲让进:当厕所空时,有想进的人总是能进
    3. 有限等待:不能无限阻塞其中一个人,当A出厕所后,如果B已经准备好,应该由B进入厕所(此时 A 无限步骤也最多只能写入标志 b,所以它无法阻止 B)
  3. 自旋锁(spin lock):基于原子操作
    1. xchg :原子指令,交换值
    2. lock(){ while( xchg(&a, 1) ); }:获得锁
    3. unlock(){ xchg(&a, 0); }: 释放锁
  4. CAS(比较然后写入)机制:
    1. 读取(并标记,如果被其他线程写入,该标志改变)
    2. 操作
    3. 写入(比较标记,失败重做1~3)
  5. 互斥锁(mutex lock):对自旋锁改进,在失败时休眠线程(依赖系统调用)
  6. 生成者-消费者模型(produce-consume):解决 90% 的线程同步问题
    1. 对生产者做限制:比如控制队列长度
    2. 消费者依赖生产者 : 1-2 实现了同步协调
  7. 条件变量(cond): 当线程任务条件没有达成时,进入休眠状态,由另外线程唤醒
    1. mutex_lock(&lk)
    2. while(n == 0) cond_wait(&cv, &lk) : 当不符合条件,释放锁,进入休眠
    3. 苏醒时自动获得锁,判断条件,直到条件满足时(n != 0),执行任务内容
    4. cond_signal(&cv) : 唤醒关联该条件变量的某个休眠中的线程
      1. cond_broadcast(&cv): 唤醒其余线程
    5. mutex_unlock(&lk)
  8. 信号量(PV元语):特殊的条件变量,因为条件逻辑固定所以不如条件变量灵活
    1. S = 1 : 类似互斥锁,> 1 类似条件变量
    2. P(s):S > 0 : s-- 并执行任务,否则休眠,即类似 s >0 的条件变量
    3. V(s): S >= 0 : s++ 否则唤醒睡眠线程
  9. 解决死锁
    1. 自主式:同时获取计算任务所需要的全部资源,而非部分
    2. 集中式:使用资源管理程序,如生产者-消费者模型
  10. 协程(可切换的函数):持有执行状态,且能暂停时退出(yield)进入时恢复(resume),从而实现类似线程的异步效果。协程本质是函数的扩展(类似闭包),所以并不真正具备多线程能力,它依赖调度器的具体实现来解决阻塞问题。
    1. async : 创建异步调用(执行状态机)
    2. yield : 暂停,并返回中间值
    3. resume : 恢复(一般交由专门的管理程序,即调度器)
    4. await : 等待另一个异步调用完成,然后 wake
    5. 有栈协程:由任务函数外创建通用状态机,提供专用栈来保存状态
    6. 无栈协程:由任务函数根据自身构造一个专用状态机。无须栈但通用性稍差
  11. epoll:IO 多路复用,解决 IO 读写的阻塞问题。
    1. FD:文件描述符
    2. select : 将判断一组文件描述符是否有数据的任务转接给内核态,该函数阻塞直到有数据
    3. poll: 改进监察的数据结构,更容易编程
    4. epoll:改进监察的数据结构,更高效率
      1. epoll_create: 创建描述表
      2. epoll_ctl: 设置表内容
      3. epoll_wait: 监察
  12. io_uring :异步 IO

基本用法

基础线程

  1. 开始线程:pthread_create(pthread_t *线程id, const pthread_attr_t *线程属性, void *(*入口函数)(void *), void *入口函数参数)
  2. 退出线程:void pthread_exit(void* 返回值)
    1. 使用退出线程并不会结束子线程
  3. 连接线程:int pthread_join(pthread_t 线程id, void **返回值)
    1. 即等待该子线程结束
  4. 分离线程:int pthread_detach(pthread_t 线程id)
    1. 分离子线程,让其自生自灭
#include <stdio.h>
#include <pthread.h> // 1. 多线程库

void *hello(char* name){ // 2. 线程启动的函数
    printf("hello %s!\n", name);
    return "hello";
}
int main(void){
    pthread_t thread; // 3. 线程 id
    void *result = (char[100]){0}; // 5. 缓冲区
    pthread_create(&thread, NULL, hello, "jim"); // 4. 创建
    pthread_join(thread, &result); // 6. 主线程等待子线程结束(同步)
    printf("result:%s \n", result); // 7. 查看返回值
    printf("hello world!\n");
    return 0;
}

互斥量

数据冲突成因:

  1. 读操作可以共享
    1. 多少个读(者)都没关系,因为数据不会改变
  2. 写操作应该唯一
    1. 写入会异步改变(另外一处)读取的结果,如导致多次读结果不一致
    2. 双写数据会变得乱七八糟
    3. 所以常规逻辑下,书写者(对一个对象)应该是唯一的独占的
  3. 同步代码是有严格顺序的,所以可以认为同一时间只有一个操作,不管是读取还是写入。当然理论上,谁也不能阻止程序员写出乱七八糟的读写逻辑,但可能性比较低,因为这种方式符合人类直觉。
  4. 异步代码是无序的,所以如果对同一个对象有一个写入者 + 另一个读者以上,可以认为他们总是同时进行,这样的数据结果是不可预料的,不确定的,这种逻辑几乎可以确定为 bug,除非客户需要乱码。
  5. 因此,对共享数据(处于异步操作中的),必须让其同步化,序列化,保证操作逻辑是明确的。
  6. 这里面的数据并不一定是单指数据,也可能广义上理解为一系列逻辑上的有序操作,应该让其同步化。简而言之,就是让某一段代码,由异步变同步。
  7. 这其实有难度的,你需要分析:
    1. 确定是否共享的数据
      1. 本地数据是没问题的,因为不同线程使用不同的堆栈,要针对从外部获取的数据
      2. 调用的子函数是否支持异步,是经过异步优化的没,否则它可能本身就依赖一些数据读写,产生逻辑错误,如果不支持那么就要整体同步化
        1. 可能对子函数的特点要有所了解,无状态的函数是可以轻易异步化的
        2. 闭包问题也不大
        3. 依赖外部状态的函数比较危险
      3. 对象、数据结构(类上)
    2. 确定业务是否存在逻辑次序
    3. 同步技术本身的复杂性

同步化:

  1. join: 上面的 join 操作就有这个效果,但颗粒比较大,控制整个线程,得等它结束,往往我们需要细化的,两个线程交替运行,中间一段保持同步。
  2. mutex: 互斥量,某段代码加锁,保持一次只有一个线程通过
    1. pthread_mutex_init(pthread_mutex_t *互斥锁, 选项)
    2. pthread_mutex_lock(锁)
    3. pthread_mutex_unlock(锁)
    4. pthread_mutex_destroy(锁)
    5. cond:条件变量,暂停线程并等待别的线程唤醒,配合锁使用。对于需要某种条件才能执行的代码(这种条件会被另一个线程改变),可以提高执行性能,因为不需要轮询条件是否满足
      1. pthread_cond_signal(pthread_cond_t *条件变量)
      2. pthread_cond_wait(条件变量,锁)
  3. 解决死锁:当持有 a 又申请 b,而持有 b 的线程又在申请 a 时,就发生死锁
    1. 只用一个锁取代 ab,当一个业务需要 ab 资源,那么它就应该一次性同时申请 ab,而不是分开成 ab 两个锁,这是上策
    2. 对锁排序,比如资源申请顺序都保持 a 永远在 b 前
    3. 使用条件变量,主动在不符合条件的情况下释放锁
// 对单个变量的锁
// 线程 ab 对 A 变量进行读写,那么它实际 r/w 同时发生
// a 读出来,做计算,然后写入。
// 结果 b 早就在期间写入一个新值,所以就丢失了 b 写入的数据
// 解决问题的关键是 A 只能被一个线程拥有,这叫变量的同步
// a 获得 A 的所有权,然后计算,然后写入。 
// b 这时候如果想读,不好意思,请等待
// 这就是对单个变量的锁的应用逻辑,也就是 A 只能被某个线程独占
// 程序角度就是:锁-读-处理-写-解锁
// 注意要包裹整体逻辑,而不能 锁-读-解锁-锁-写-解锁 这样设计
// 解锁之后,b 就可以写入,a 早前读取的数据就失去意义了

// 对多个变量的锁
// 对多个变量运用单个锁是安全的
// 程序逻辑: 锁-读A-写A-读B-写B-解锁
// 就是同步范围有点大,导致 AB 的处理都只能在单线程

// 对单种顺序的锁
// 当我们希望一段逻辑有序(同步)时,比如打印 a 再打印 b
// 但多线程下 ab 都可能发生,如 aababbba
// 可以将其理解为:对多个变量使用单个锁
// 程序逻辑: 锁-a-b-解锁

// 对多种顺序的锁
// 要么 ab 要么 ba
// ab 或 ba 可以理解为多个变量
// 对多个变量使用单个锁,就能产生互斥(同步)的效果
// 程序逻辑: 锁-ab-解锁-锁-ba-解锁

// 两个锁(避免死锁的方法)
// 任意对象单个锁无疑是最强的同步,但是灵活性(并行性)受到约束
// 这就要求我们对对象进行分组来加不同的锁
// 比如每个变量分别配不同的锁,这就杜绝了数据竞争的问题

线程局部数据

  1. 线程独有数据是安全的,又是函数外部范围可共享的数据,只属于单个线程所有。
    1. int pthread_key_create(pthread_key_t *线程私有变量, void(* 清理函数)(void *))
    2. int pthread_setspecific(线程私有变量, const void* 设置值)
    3. int pthread_key_delete(线程私有变量)

参考

  1. pthread介绍:https://www.cnblogs.com/yuanqiangfei/p/15268698.html
  2. c语言pthread库_线程库:https://ispacesoft.com/57898.html
  3. 条件变量为什么要和互斥锁一起使用:https://blog.csdn.net/yizhiniu_xuyw/article/details/109635912
  4. 南京大学2022操作系统(蒋炎岩):https://www.bilibili.com/video/BV13u411X72Q
  5. 【并发】IO多路复用select/poll/epoll介绍:https://www.bilibili.com/video/BV1qJ411w7du
  6. 操作系统层面聊聊BIO,NIO和AIO (epoll):https://www.cnblogs.com/twoheads/p/10712094.html
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助