课程的讲解顺序
- 单核向量–>多线程–>多个节点分布式编程–>cuda/gpu编程
多线程Pthread编程
Pthread编程(POSIX): 共享内存式的编程 (书上第四章内容)
共享内存和分布式内存回顾
Pthreads编程简介
- 基本概念
- 基础API
- 同步
共享内存系统
多个cpu通过一个互联网络与内存连接, 每一个cpu都能访问内存中每一块区域, cpu通过访问共享数据进行隐性通信, 而不是cpu之间直接通信.
- 一致内存访问(UMA)多核系统
- 非一致内存访问(NUMA)多核系统
Cache一致性的解决方法
- 基于侦听的Cache协同(每次改变通知所有核心该变量被更新了, 无关的核心也会被影响)
- 所有核心共享总线
- 总线上传输的任何信号都能被连接到总线的所有核心“看到”
- 基于目录的Cache协同
- 使用一种称为目录的数据结构保存每个cache line 的状态
- 当一个变量 被更新时, 就会查询目录, 对缓存了该变量的核心, 其缓存的副本的状态置为无效.
- 程序员无法直接控制Cache, 也无法控制Cache如何更新
- 但是程序员可以重新组织访存模式, 来更好的利用cache, 比如可以按行读取就不要按列读取
伪共享
- cpu从内存中读取数据时, 并非是一个机器字一个机器字的读取, 而是以缓存行为单位, 直接读取所需变量所在的一个缓存行.
- 一个cache line 中包含多个机器字
- 当多个cpu访问同一个cache line时, 即使访问的是不同的机器字, 看起来也有潜在的竞争条件, 产生不必要的开销.
书上的例子
共享互联网络
- 总线
- 一组并行通信线路 + 总线访问控制 硬件
- 连接到总线的设备共享通信线路
- 随着连接到总线的设备数量增加, 使用竞争就会加剧, 性能会下降
- 交换互联
- 使用交换开关控制数据在设备间的路由
- 交叉开关 Crossbar
具体示意图回顾第二讲
共享内存编程
- 动态线程 (市场经济 )
- 主线程等待计算工作, fork新线程分配工作, 工作线程完成任务后结束
- 资源高效利用, 但线程的创建和结束非常耗时
- 静态线程 (计划经济)
- 创建线程池, 并向其中线程分配任务, 单线程不结束, 直至整个程序结束
- 性能更优, 但可能浪费系统资源
资源高效利用 和 性能优化 两者不可兼得
并行程序设计的复杂性
- 足够的并发度(Amdahl 定律)
- 并发粒度
- 独立计算任务的大小
- 局部性
- 对临近的数据进行计算
- 负载均衡
- 处理器的工作量相近
- 协调和同步
- 谁负责? 处理频率?
并行程序的性能评价
- 加速比
- 效率
- 可扩展性
多线程编程
- 两种用于并行编程的库, 并非编程语言, 而是一些列API (类似于c和c++的关系)
- Pthread是POSIX标准
- 相对底层, 程序员可以控制的更多, 也相对较复杂
- 程序员控制线程管理和协调
- 程序员分解并行任务并管理任务调度
- 可移植 但比较慢
- 在系统级代码开发中广泛使用
- OpenMP 是新标准
- 高层编程, 集成度更高, 程序员只需要写一些简单的指令, 编译器可自动执行大多数工作, 适用于共享内存架构上的科学计算
- 程序员在较高层次上指出并行方式和数据特性, 指导任务调度
- 系统负责市级的并行任务分解和调度管理
- 多种架构相关的编译指示
- POSIX: Portable Operating System Interface for UNIX 操作系统工具接口, UNIX等系统上的一个标准库
- Pthread: 基于POSIX的线程接口
Pthread API
- pthread_t : 相当于存储线程相关信息的一个结构体, 里面的数据由系统操控, 程序员无法直接操控
pthread_create(pthread_t *, const pthread_attr_t*, void* (*)(void *), void *) : 创建子线程
- 调用示例
- errcode = pthread_create(&thread_id, &thread_attribute, &thread_fun, (void*)&fun_arg);
- 解释
- thread_id: pthread_t的类型, 表示创建的线程的ID或句柄(用于控制线程)
- thread_attribute: 各种属性, 通常用空指针NULL表示标准默认值属性
- thread_fun: 新线程要运行的函数 (参数和返回值类型都是void*)
- fun_arg: 传递给要运行的函数thread_fun的参数
- errcode: 若创建失败, 返回非零值. 成功返回0
- 作用
- 主线程借助操作系统创建一个新线程
- 线程执行一个特定函数thread_fun
- 如果所有创建的子线程执行相同的函数, 则表示主线程的计算任务分解
- 如果不同的子线程需要执行不同任务, 则可用创建线程时传递的参数区分线程的“ID”以及其它线程的独特属性
- 注意
- 第三个参数的函数类型!!!返回值和参数值都是void*类型
- 创建和结束新线程的开销非常大, 远大于一次浮点数运算, 因此需要执行的子线程中的函数应该是解决一定规模问题的函数, 不然得不偿失.
pthread_join(pthread_t , void **value_ptr) : 将线程加入主线程表示子线程的停止
- 说明
- 第一个参数表示停止线程的句柄, pthread_t类型
- 第二个参数与许目标线程退出时返回信息给调用线程 (子线程执行的函数的返回值)
pthread_exit(void * value_ptr): 显示停止线程, 通过value_ptr返回结果给调用者
pthread_cancle(pthread_t thread)
- 在主线程中调用
- pthread_cancel(thread)
- pthread_join(thread, &tstatus)
- if (status == PTHREAD_CANCELED) {}
- 在子线程中调用接收函数pthread_testcancel();
- 两者需要成对使用
Pthread编程中的同步
- 临界区
- 更新共享资源的 代码段, 一次只能允许一个线程执行该代码段
- 原子性:
- 一组操作要么全部执行, 要没有全不执行
- 竞争条件:
- 多个进程/线程 尝试更新同一个共享资源时, 结果可能无法预测, 则存在竞争条件.
- 存在竞争条件时, 需要对临界区加锁.
- 数据依赖
- 两个内存操作的序, 为了保证结果的正确性, 必须保证这个序
- 同步
- 在时间上强制使各执行进程/线程再某一点必须互相等待, 确保各进程/线程 的正常顺序和对共享可写数据的正确访问
- 解决竞争条件的两种方法的比较:
- 同步–忙等待方法: 指定顺序进入临界区
- 显示同步–互斥量(锁): 先到先进入临界区
- 互斥量是阻塞等待, 在锁外等待的进程是临时挂起不占用cpu资源的, cpu可以执行其余事情
- 忙等待, 等待执行的进程/线程仍然在占用cpu资源.
- 互斥量的死锁状况
thread1 thread2
lock(a) lock(b)
lock(b) lock(a)
-
链表CURD的并行操作
- 链表级的加锁 (粒度太粗)
- 节点级的加锁 (加锁解锁操作开销很大)
- pthread中的读写锁
-
链表CURD, 性能对比
- 横向线程数
- 纵向读写锁, 链表锁, 节点锁
- 查询, 插入, 删除 的比例变化
疑惑
- 为什么mac上并行的速度都慢于串行???
- pthread_join 和 pthread_exit的结合使用???
- 为什么centos上规模越大速度越快???