(原文见:https://computing.llnl.gov/tutorials/pthreads/)
概述
在共享内存多处理器架构中,线程被用来实现并行.发展历程中,硬件供应商实现了它们自己的私有线程版本来方便软件开发者。对于UNIX系统, IEEE POSIX 1003.1c(1995)标准制订了标准化的C语言线程编程接口.此标准的相应实现被称为POSIX线程或者Pthreads.
本教程首先介绍了相关概念、 动机及使用 Pthreads 设计注意事项。稍后涵盖PthreadsAPI中的三个主要类别的每个例程︰线程管理、互斥变量和条件变量。本文通篇使用示例代码以揭示如何使用大多数的Pthreads例程(这是Pthreads新手程序员所需要的)。本教程最后讨论LLNL细节和如何将pthreads与MPI混合。也包括实验练习以及众多的示例代码(C语言)。
预备知识:本文针对使用线程进行并行编程的新手.对使用C进行并行编程有个基本了解是必要的.对于那些对并行编程很陌生的人, EC3500: Introduction to ParallelComputing提供的材料会有所帮助.
Pthreads Overview
什么是线程?
对软件开发者,独立于它的主程序的“程序( procedure)”这个概念也许是对线程最好的描述.
动身之前,想象一下:一个主程序(a.out)包含数个“程序”,这些程序都能被操作系统同时或者独立调度。这就是一个“多线程”程序.
这是如何达成的?
- 环境
- 工作路径
- 程序指令
- 寄存器
- 栈
- 堆
- 文件描述符
- 信号动作
- 共享库
- 进程间通信工具 (message queues, pipes, semaphores, 或者 shared memory).
- 栈
- 寄存器
- 调度属性(策略、优先级)
- 挂起或者阻塞的信号集
- 线程特有数据.
- 在父进程存在期间拥有自己独立的控制流,得到操作系统的支持
- 只复制独立调度所需的基本资源
- 和其它线程共享进程资源
- 随父进程消亡
- “轻量级”,因为大多数开销已经由进程的创建完成
为什么使用Pthreads?
轻量级
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 |
C Code for fork() creation test
==============================================================================
#include <stdio.h>
#include <stdlib.h>
#define NFORKS 50000void do_nothing() {
int i;
i= 0;
}int main(int argc, char *argv[]) {
int pid, j, status;for (j=0; j<NFORKS; j++) {/*** error handling ***/if ((pid = fork()) < 0 ) {printf ("fork failed with error code= %d\n", pid);exit(0);}/*** this is the child of the fork ***/else if (pid ==0) {do_nothing();exit(0);}/*** this is the parent of the fork ***/else {waitpid(pid, status, 0);}}
}
==============================================================================
C Code for pthread_create() test
==============================================================================
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>#define NTHREADS 50000void *do_nothing(void *null) {
int i;
i=0;
pthread_exit(NULL);
} int main(int argc, char *argv[]) {
int rc, i, j, detachstate;
pthread_t tid;
pthread_attr_t attr;pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);for (j=0; j<NTHREADS; j++) {rc = pthread_create(&tid, &attr, do_nothing, NULL);if (rc) { printf("ERROR; return code from pthread_create() is %d\n", rc);exit(-1);}/* Wait for the thread */rc = pthread_join(tid, NULL);if (rc) {printf("ERROR; return code from pthread_join() is %d\n", rc);exit(-1);}}pthread_attr_destroy(&attr);
pthread_exit(NULL);}
- 在高性能计算机上使用线程的主要动机就是获得最佳性能. 尤其程序使用MPI进行无节点通信,使用线程会有质的提升
- MPI 库通常使用共享内存实现无节点任务通信, 这至少会有一次内存复制操作 (进程到进程).
- 对于线程,就没有内在的内存拷贝需求,因为线程共享单一处理器的同一地址空间.无需数据传输,就像传递指针一样高效.
- 最坏场景,线程通信会遇到 cache-to-CPU 或者 memory-to-CPU 带宽问题. 即便如此,它的速度还是远高于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 |
- 与非线程化得程序相比,线程化的程序在如下几个方面提供潜在的性能提升和实际优点:
- 使CPU与I/O重叠工作: 例如,程序的一个线程在等待长时间I/O系统调用完成的同时,其它线程可以使用CPU进行密集计算.
- 优先/实时调度:更重要的任务能取代或者打断低优先级的任务.
- 异步事件处理: tasks which service events of indeterminate frequency and duration can be interleaved. 例如,web服务器能同时响应先前的请求传输数据以及控制新到来的请求.
- 一个完美的例子就是浏览器,同一时间很多任务加塞执行(现在的浏览器都是独立进程了).
- 另一个好例子是现代操作系统,将线程用到了极致。下面是大家熟悉的win7的资源管理器截图.
设计线程化的程序
在现代多核机器上,pthreads是并行计算的理想选择.并行编程有很多要素要考虑,比如:- 使用什么并行编程模型?
- 问题分解(切分)
- 负载均衡
- 通信
- 数据依赖
- 同步和条件竞争
- 内存问题
- I/O 问题
- 问题复杂度
- 程序员的努力、花费、时间
- ...
通常来讲,一个程序要从线程中获益,它应该能被分为独立的任务.
例如, 如果 routine1 和 routine2 能被互换或者在时间上重叠进行, 那么他们就能被线程化.
- 执行的工作或者操作的数据能被多个任务并行执行:
- 潜在的长时间I/O 阻塞等待
- 在一些地方占用了大量CPU时间,但是别的缺空闲
- 必须响应异步事件
- 一些工作比另一些更重要 (优先级中断)
- Manager/worker: 一个单线程的 manager 给其他线程( workers)分配工作. 典型的,控制者处理所有输入,并将工作打包分配给其他任务. 控制者/工作者模型至少有两种常见模型: 静态工作者池和动态工作者池.
- Pipeline: 一个任务被分解为一系列的子操作,每个操作都被串行化处理,但是所有子操作被不同线程并行执行. 自动装配线是这个模型的最佳描述.
- Peer: 类似 manager/worker 模型,但是当主线程创建了其他线程后,它自己也参与工作.
- 所有线程访问同一个全局、共享内存
- 线程拥有自己的私有数据
- 编程人员负责同步全局共享数据的访问.
例如,设想你的程序创建了好几个线程,每个都会调用同一个库例程:
- 这个库例程(routine)访问/修改一个内存中的全局结构或者位置.
- 当这些线程调用此例程时都有可能会试图在同一时间试图修改这个全局结构体/内存位置.
- 如果这个例程不采用某种同步构造来防止数据损坏,那么它就不是线程安全的.
建议:小心使用哪些没有明显保证线程安全的库或者对象.如果存在疑问,就假定他们不是线程安全的,直到证明为止.
- 虽然Pthreads API 是ANSI/IEEE标准,实现却能不以标准制定的方式完成(事实上它们也常这么做).
- 正因为如此,在一个平台上运行良好的程序,在另外的平台上却可能问题不断.
- 例如,当你设计程序时,线程的最大数量以及线程默认栈大小是要考虑的两个重要限制.
- 随后,本位将讨论几个线程限制的进一步的细节.
Pthreads API
ANSI/IEEE posix1003.1-1995 标准定义了原始的 Pthreads API。POSIX 标准一直在演变,并进行修改,包括 Pthreads 规范.标准的副本可以从 IEEE 购买或从其他网站在线免费下载.
组成Pthreads API 的子课题可被分为四个主要的组:
- 线程管理︰ 直接对线程的创建、 分离、 加入等工作的例程。包含来设置/查询线程属性的函数 (joinable,调度等.)
- Mutexes: 处理同步,称为“互斥锁”的例程,是“mutual exclusion"的缩写。互斥锁函数提供创建、 销毁、 锁定和解锁互斥锁。这些都被辅之以设置或修改与互斥对象关联的属性的互斥锁属性函数.
- Condition variables: 解决线程间共享互斥体通讯的例程.基于程序员指定的条件.包括一组基于指定的可变值创建,销毁,等待以及发送信号的函数,也包括设置/查询条件变量属性的函数.
- Synchronization: 管理读写锁以及桩(barriers)的例程.
前缀 |
函数组 |
pthread_ |
Threads themselves and miscellaneous subroutines |
pthread_attr_ |
Thread attributes objects |
pthread_mutex_ |
Mutexes |
pthread_mutexattr_ |
Mutex attributes objects. |
pthread_cond_ |
Condition variables |
pthread_condattr_ |
Condition attributes objects |
pthread_key_ |
Thread-specific data keys |
pthread_rwlock_ |
Read/write locks |
pthread_barrier_ |
Synchronization barriers |
Pthreads API 大约包含了100个子例程。本文只侧重于其中的一部分,特别是那些开始pthreads编程的程序员可能马上就需要使用的。
为可移植性,应在每个使用 Pthreads 库的源文件中包括 pthread.h 头文件.
当前的 POSIX 标准是只为 C 语言定义的。Fortran 语言的程序员可以使用 C 函数调用的包装。一些 Fortran 编译器可能提供 Fortran pthreads.
Pthreads 优秀书籍宛如繁星。在本教程中的引用部分中会列出部分.
编译线程化的程序
编译器/ 平台 |
编译器命令 |
描述 |
INTEL/Linux |
icc -pthread |
C |
icpc -pthread |
C++ |
|
PGI/Linux |
pgcc –lpthread |
C |
pgCC -lpthread |
C++ |
|
GNU Linux, Blue Gene |
gcc -pthread |
GNU C |
g++ -pthread |
GNU C++ |
|
IBM Blue Gene |
bgxlc_r / bgcc_r |
C (ANSI / non-ANSI) |
bgxlC_r, bgxlc++_r |
C++ |