[并行程序设计] 多线程pthread编程笔记

课程的讲解顺序

  • 单核向量–>多线程–>多个节点分布式编程–>cuda/gpu编程

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)

疑惑

  • 为什么mac上并行的速度都慢于串行???
  • pthread_join 和 pthread_exit的结合使用???
  • 为什么centos上规模越大速度越快???

猜你喜欢

转载自blog.csdn.net/weixin_40996518/article/details/105587117