简介 |
在共享内存的多处理器结构中,可以用线程来实现并行。对于UNIX系统, IEEE POSIX
1003.1c标准规定了C语言线程编程接口的标准。这份标准的实现就是POSIX threads, 或者称为Pthreads.
本文开始先介绍线程的基本概念,动机和设计方面的一些考虑。 接下来是Pthreads API 的三个主要部分:线程管理,互斥锁和 条件变量。本文自始至终会贯穿大量的示例代码来展示如何使用Pthread API的每一部分。
Pthreads 概述 |
线程是什么?
- 从技术角度讲,一个线程是一个独立的指令流可以被操作系统调用运行。But what does this mean?
- 从程序员的角度讲,独立于主程序而单独运行的“程序”可以被称为一个线程。
- 更进一步的讲, 想象一个包含很多子程序的主程序 (a.out) .
然后想象所有这些子程序都能操作系统调用同时并且独立的运行。这就是一个多线程的程序。
- 这是怎么完成的呢?
- 在理解线程之前, 先要理解UNIX系统下的进程。 一个进程是由操作系统创建的, 并且要求大量的"开销"。进程包含关于程序资源的信息和程序的执行状态,包括:
- 进程ID,进程组ID, 用户ID,和组ID
- 环境
- 工作目录
- 程序指令
- 寄存器
- 栈
- 堆
- 文件描述符
- 信号操作
- 共享库
- 进程通信工具 (例如消息队列,管道,信号量,或者共享内存 ).
UNIX进程 线程
-
线程使用并存在在这些进程资源中,线程可以被操作系统调用并独立运行, 很大程度上是因为它只复制了很少量的作为可执行代码存在的必须重要资源。
-
一个线程完成独立的控制流是因为它维护自己拥有的:
- 栈指针
- 寄存器
- 调度优先级
- 待定和阻塞信号集合
- 线程特有的数据
-
因此,简而言之,在UNIX系统环境中一个线程:
- 存在在一个进程中并使用进程资源
- 只要父进程存在并且操作系统支持就一直拥有自己独立的控制流
- 仅复制独立调度所必须关键资源
- 可以和其他线程共享进程资源
- 如果线程的父进程死掉,则该线程死掉
- 是“轻量级”的因为大多是开销在它的进程创建的时候已经完成了
-
同一进程的线程间共享资源,导致:
- 一个线程对于共享的系统资源所做的更改(例如关闭一个文件)将会被所有其他的线程发现
- 两个具有相同值的指针指向相同的数据
- 可能读写相同的内层位置,因此要求程序员进行显式的同步
为什么使用线程
- 在高性能计算的世界,使用线程的首要目的是实现潜在的程序性能的提升
-
与创建和管理进程的花费相比,创建一个线程只需要较少的系统开销。管理线程所需要的系统资源原少于管理进程
举例来说,下表是fork() 与 pthread_create()所花时间的对比。
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 |
-
一个进程中的所有线程共享相同的地址空间。在很多情况下线程间通信比进程间通信更高效和容易。
-
线程应用与非线程应用相比,提供的潜在的性能提升和实际的优势主要通过以下几种方法:
- 通过I/O重叠CPU工作:
- 优先/实时调用:更重要的任务可以优先调用或者中断低优先级的任务
- 异步事件处理:服务不确定频率和交叉存取的事件的任务。举例来说,一个web服务端程序可以在和一个请求传送数据的同时管理新到的请求
设计线程程序
并行编程
- 设计并行程序需要考虑很多方面:
- 使用什么类型的并行编程模型
- 问题分解
- 负载平衡
- 通信
- 数据依赖关系
- 同步和竞争条件
- 内存问题
- I/O问题
- ...
- 本文将不对上述问题进行深入的探讨,不过,感兴趣的可以戳这里:Introduction to Parallel Computing
- 一般来讲,一个程序想要发挥Pthread的优势,必须是可以分离成独立的可以同时运行的子任务 如图所示:
- 适合使用Pthread的程序一般由如下性质:
- 工作可以被多任务同时执行或者数据可以被同时操作
- 潜在的长时间I/O等待阻塞
- 必须响应异步事件
- 一些工作比其他的工作更重要(优先级中断)
- 一些常见的基于线程的程序模型:
- Manager/worker:manager线程分配任务给其他workers线程。
- Pipeline:一个任务被分成一系列的子任务,每个子任务都被一个不同的线程有序并发的处理。类似汽车装配线
- Peer:有点类似Manager/worker,但是,在主线程建立其他线程之后,就各自工作
共享内存模型:
- 所有线程都可以访问相同的全局,共享内存
- 线程可以由自己的私有数据
- 程序员应该负责同步全局共享的数据的访问
线程安全
- 线程安全:
- 举例,假设你的程序创建一些线程,每个线程都调用相同的子程序:
- 这个子程序访问或者更改一个全局结构或者一个全局变量
- 由于每个线程都调用这个子程序,所以它们可能同时尝试修改这个全局结构或者全局变量
- 如果这个子程序没有使用某种同步方法来防止数据崩溃的话,这个程序就不是线程安全的