Pthreads多线程编程(1)

本系列的学习笔记是参考的Pthreads教程

一 引言
在支持共享内存的多核架构中,线程(thread)可用来实现并行编程。从历史的角度来看pthread的诞生,硬件厂商在各自的产品上对线程进行实现,这样就给软件开发人员提出了一个难题(没有统一的标准,开发的时候如何兼容)。基于此,一个标准化的C语言线程编程接口被提出来了——IEEE POSIX 1003.1c标准,这个标准是用于UNIX操作系统的。紧跟这这个标准,线程的实现也应运而生,就是我们熟知的POSIX  threads,或者叫pthreads.

二 Pthreads 概览
     1.什么叫线程?
  • 技术上解释,一个线程定义为一系列独立的指令流,它们按照操作系统制定的某种顺序执行,但是具体来说是什么意思呢?
  • 对于软件开发人员,一个独立于主程序(main program)运行的过程(procedure)是线程的最好描述。
  • 再进一步,例如一个主程序a.out(Linux操作系统中的可执行程序)包含一系列的过程,然后假设所有的过程都按照操作系统制定的顺序同时地或者独立地运行,这就是一个“多线程”程序。
  • 这种同时/独立运行是怎样完成的呢?
  • 在理解线程之前,我们需要先理解UNIX process(进程)。进程是由操作系统创建的,它会申请一定的“开销”(内存?)。一个进程包含有关程序资源和程序执行状态的信息:
    • 进程ID,进程组ID,用户ID,组ID
    • 环境
    • 工作目录
    • 程序指令
    • 寄存器
    • 文件描述字(File Descriptors)
    • 信号操作
    • 共享库
    • 进程间通信工具(例如消息队列,管道,信号量,共享内存

Figure 1 Unix进程


Figure 2 Unix进程中的线程
  • 线程使用这些资源,它可以被操作系统scheduled并且作为一个独立的实体运行,因为它会从主程序里拷贝最基本的资源,使得自己成为可执行的一段代码。
  • 线程需要维持自己的资源,如下:
    • 栈指针
    • 寄存器
    • 进度属性(例如policy,优先级)
    • 挂起和阻塞的信号集
    • 线程特有的数据
  • 总结一下,在Unix/Linux操作系统中,一个线程的特征有:
    • 存在于进程中,并且使用进程的资源
    • 具有独立的控制流(理解为代码执行顺序),前提是父进程存在并且操作系统支持
    • 只从父进程拷贝最基本的资源以供自己独立执行
    • 可能会和其他线程共享父进程资源
    • 在父进程被杀死时,线程死掉
    • 它是轻量的,因为大部分的开销都被父进程创建时占有。
  • 由于同一部进程内的线程会共享资源,那么:
    • 由线程1引起的对进程资源的修改(例如关闭文件)对于其他线程是可见的。
    • 两个相同的指针指向同一个数据。
    • 读取/写入同一个内存区域是可能的,因此需要程序员严格控制同步。
     2. 什么是Pthreads?
  • 为了最大化地利用线程带来的好处,一个标准化的接口被提出。
    • 对于UNIX系统,这个接口即是 IEEE POSIX 1003.1c标准(1995)。
    • 和这个标准无缝贴合的实现就是所谓的POSIX threads,简称Pthreads。
    • 当前大多数的硬件提供商在原来的硬件API基础上提供了对Pthreads的支持。
  • Pthreads定义为一系列C语言编程类型和过程调用的集合,在实现时,需要加上头文件pthread.h和thread库的支持(这个库有可能是其他标准库的一部分,如libc)。
     3. 为什么选用Pthreads?
  • 轻量级:
    • 和进程创建相比,线程创建时消耗更小的操作系统开销。同时,和管理进程相比,管理线程需要更少的系统资源。
    • 我们对比两种建立子程序时所需的时间消耗,分别是fork()——建立子进程和pthread_create()——创建线程。测试时分别创建5000个子进程/线程,记录测试结果的单位是秒,没有加入优化标志。
  • Platform fork() pthread_create()
    real user sys real user sys
    Intel 2.6 GHz Xeon E5-2670 (16 cores/node) 8.1 0.1 2.9 0.9 0.2 0.3
    Intel 2.8 GHz Xeon 5660 (12 cores/node) 4.4 0.4 4.3 0.7 0.2 0.5
    AMD 2.3 GHz Opteron (16 cores/node) 12.5 1.0 12.5 1.2 0.2 1.3
    AMD 2.4 GHz Opteron (8 cores/node) 17.6 2.2 15.7 1.4 0.3 1.3
    IBM 4.0 GHz POWER6 (8 cpus/node) 9.5 0.6 8.8 1.6 0.1 0.4
    IBM 1.9 GHz POWER5 p5-575 (8 cpus/node) 64.2 30.7 27.6 1.7 0.6 1.1
    IBM 1.5 GHz POWER4 (8 cpus/node) 104.5 48.6 47.2 2.1 1.0 1.5
    INTEL 2.4 GHz Xeon (2 cpus/node) 54.9 1.5 20.8 1.6 0.7 0.9
    INTEL 1.4 GHz Itanium2 (4 cpus/node) 54.5 1.1 22.2 2.0 1.2 0.6
  • 高效率的通信/数据交换
    • 在多核架构上考虑采用Pthreads的最主要的原因是想获得最佳的系统性能。尤其是对于某些使用MPI(Message Passing Interface)作为已知节点通信的应用程序,可以使用Pthreads(替换MPI)来获得潜在的性能提升。
    • MPI通常是通过共享内存来实现已经节点的任务间通信,这样至少会引入内存拷贝操作(进程间的)。
    • 而对于Pthreads,由于线程在同一个进程内共享相同的地址,因此不需要MPI的中间内存拷贝操作。没有实际的数据转移过程,Pthreads中的数据交互就和传递指针一样高效。
    • 考虑在最糟糕的场景,Pthreads的通信速度依赖于缓存-cpu或者内存-CPU过程中的带宽。即便如此,Pthreads通信的速度也高于MPI共享内存通信。
    • 下表列出了最糟糕情况下Pthreads的通信速度与MPI共享内存的速度对比,选取了不同时期的处理器平台。
  • Platform MPI Shared Memory Bandwidth
    (GB/sec)
    Pthreads Worst Case
    Memory-to-CPU Bandwidth 
    (GB/sec)
    Intel 2.6 GHz Xeon E5-2670 4.5 51.2
    Intel 2.8 GHz Xeon 5660 5.6 32
    AMD 2.3 GHz Opteron 1.8 5.3
    AMD 2.4 GHz Opteron 1.2 5.3
    IBM 1.9 GHz POWER5 p5-575 4.1 16
    IBM 1.5 GHz POWER4 2.1 4
    Intel 2.4 GHz Xeon 0.3 4.3
    Intel 1.4 GHz Itanium 2 1.8 6.4
  • 其他原因
    • 相比于非线程应用程序,线程应用程序从下面几个方面获得潜在的性能增益和优点:
      • 在I/O操作的同时复用CPU:例如,一个程序中的某些部分有可能在进行一个很久的I/O操作,这个时候用线程A来等待I/O完成,其他线程就可以利用CPU做复杂的计算操作。
      • 优先级/实时性调度:更重要的任务可以被调度,取代或者中断低优先级的任务。
      • 异步事件处理:服务于不确定频率和间隔的时间的任务可以交叉进行。例如,一个网络服务器可以传输数据来响应前一个请求,并同时管理刚到的新请求。
    • 网络服务器和操作系统都是很好的多线程使用的例子。
     4. 设计线程程序
  • 并行编程
    • 在当前的程序设计中,多核的CPU加上pthreads是实现并行编程的理想工具。任何适用于并行编程的都适用于并行线程编程。
    • 在设计并行编程时,我们需要考虑:
      • 采用何种并行编程模型
      • 故障分区
      • 负载平衡
      • 通信
      • 数据依赖
      • 同步和抢占条件
      • 内存问题
      • I/O问题
      • 编程复杂度
      • 程序员的时间/开销
      • ...
    • 考虑这些话题已经超出了本教程的范围,回到Pthreads。一般来说,必须是能分解成离散、独立的并同时执行的多任务的程序才能充分利用Pthreads带来的性能提升。如下图,如果子程序1和子程序2可以实时交替运行,那么它们适合用线程实现。

Figure 3 分解为线程实现的程序示例
    • 具有以下特征的程序适合用pthreads实现:
      • 工作或者数据可以用由任务并发执行或者处理。
      • 可能会被潜在的长时间的I/O操作阻塞。
      • 只是在特定的代码处消耗很多的CPU周期。
      • 必须对异步事件进行响应。
      • 任务存在优先级的区别(优先级打断的概念)
    • 有如下几种常见模型可用于线程编程:
      • 管理者/工人:管理者作为一个独立的线程,为其他线程(工人)分配任务。一般来讲,管理者处理所有的输入并将工作划分至其他的工人。至少有两种形式的管理者/工人模型是相同的:静态worker pool和动态worker pool
      • 流水线:一个任务被分成一系列的子操作,每一个子操作由不同的线程并行执行。参见汽车的自动组装线。
      • 点模型:和管理者/工人模型类似,只是在主线程创建好了其他线程后,它也会加入工作。
  • 共享内存
    • 所有的线程反正同一个全局的共享内存。
    • 每个线程有私有数据。
    • 程序员需要对全局共享数据的同步访问(保护)进行处理。
  • 线程安全
    • 简而言之,指的是程序具有的同时执行多线程并且不会“破坏”共享数据或者产生“竞争”条件的能力。
    • 例如,一个应用程序创建了几个线程,每一个线程都调用同一个库的函数:
      • 这个库的函数会访问/修改内存中的某个全局的数据。
      • 由于每一个线程都需要调用这个库函数,那么可能出现的情况是它们会在同一时刻荣是去修改那个全局的数据。
      • 如果这个库函数没有引入某种同步创建机制来防止数据破坏,那么它就不是线程安全的。

Figure 4 多线程访问同一个库函数
      • 如果你没法100%保证需要调用的外部库函数是线程安全的,那么就得想办法处理可能出现的问题。如果存在疑问,可以尝试通过"serializing the calls to the uncertain routine"去证明这个外部库函数是否是线程安全的。
  • 线程的限制
    • 即使Pthreads API是一个ANSI/IEEE标准,针对它的实现也可以各不相同。
    • 程序的移植性需要考虑,在平台A上可以正常运行,在平台B上有可能就不能运行。
    • 平台最多允许的线程数和默认的线程堆栈大小是两个重要的限制,在设计程序时需要考虑。
(未完待续,下一篇文章将对线程的创建、销毁以及属性对象进行讲解)

猜你喜欢

转载自blog.csdn.net/wblgers1234/article/details/38374505