当前位置: 代码迷 >> 综合 >> POSIX 线程编程指南(一)
  详细解决方案

POSIX 线程编程指南(一)

热度:43   发布时间:2024-01-09 03:51:29.0

(原文见:https://computing.llnl.gov/tutorials/pthreads/)

概述

    共享内存多处理器架构中,线程被用来实现并行.发展历程中,硬件供应商实现了它们自己的私有线程版本来方便软件开发者。对于UNIX系统, IEEE POSIX 1003.1c1995)标准制订了标准化的C语言线程编程接口.此标准的相应实现被称为POSIX线程或者Pthreads.

    本教程首先介了相关概念、 动机及使用 Pthreads 设计注意事。稍后涵盖PthreadsAPI中的三个主要别的每个例程线程管理、互斥变量和条件变。本文通篇使用示例代码以揭示如何使用大多数的Pthreads例程(这是Pthreads新手程所需)本教程后讨论LLNL细节和何将pthreadsMPI混合。也包括实验习以及众多的示例代码(C语言

    预备知识:本文针对使用线程进行并行编程的新手.对使用C进行并行编程有个基本了解是必要的.对于那些对并行编程很陌生的人, EC3500: Introduction to ParallelComputing提供的材料会有所帮助


Pthreads Overview

什么是线程?

技术上讲,线程就是能被操作系统调度运行的独立的指令流.但是这意味着什么?
对软件开发者,独立于它的主程序的“程序( procedure)”这个概念也许是对线程最好的描述.
动身之前,想象一下:一个主程序(a.out)包含数个“程序”,这些程序都能被操作系统同时或者独立调度。这就是一个“多线程”程序.
这是如何达成的?
要理解线程,先理解UNIX进程. 进程由操作系统创建,并且需要一定数量的“经费”。进程包含了程序资源和程序的执行状态,
包含:Process ID, process group ID, user ID, 和group ID
  • 环境
  • 工作路径
  • 程序指令
  • 寄存器 
  • 文件描述符
  • 信号动作
  • 共享库
  • 进程间通信工具 (message queues, pipes, semaphores, 或者 shared memory).

之所以达成独立的控制流,是因为线程维护着自己的:
  • 寄存器
  • 调度属性(策略、优先级)
  • 挂起或者阻塞的信号集
  • 线程特有数据.
所以,总体来说,在UNIX环境中的线程:存在于进程中,使用进程资源
  • 在父进程存在期间拥有自己独立的控制流,得到操作系统的支持
  • 只复制独立调度所需的基本资源
  • 和其它线程共享进程资源
  • 随父进程消亡
  • “轻量级”,因为大多数开销已经由进程的创建完成

为什么使用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 问题
  • 问题复杂度
  • 程序员的努力、花费、时间
  • ...
本文并不打算介绍并行编程,感兴趣的可以google.
通常来讲,一个程序要从线程中获益,它应该能被分为独立的任务. 
例如, 如果 routine1 和 routine2 能被互换或者在时间上重叠进行, 那么他们就能被线程化.
具备如下特征的程序适合线程化:
  • 执行的工作或者操作的数据能被多个任务并行执行:
  • 潜在的长时间I/O 阻塞等待
  • 在一些地方占用了大量CPU时间,但是别的缺空闲
  • 必须响应异步事件
  • 一些工作比另一些更重要 (优先级中断)
几个现有的多线程模型:
  • Manager/worker: 一个单线程的 manager 给其他线程( workers)分配工作. 典型的,控制者处理所有输入,并将工作打包分配给其他任务. 控制者/工作者模型至少有两种常见模型: 静态工作者池和动态工作者池.
  • Pipeline: 一个任务被分解为一系列的子操作,每个操作都被串行化处理,但是所有子操作被不同线程并行执行. 自动装配线是这个模型的最佳描述.
  • Peer: 类似 manager/worker 模型,但是当主线程创建了其他线程后,它自己也参与工作.
共享内存模型:
  • 所有线程访问同一个全局、共享内存
  • 线程拥有自己的私有数据
  • 编程人员负责同步全局共享数据的访问.

线程安全:指程序同步执行多个线程而不“破坏”共享数据或者导致“竞争”条件的能力.
例如,设想你的程序创建了好几个线程,每个都会调用同一个库例程:
  • 这个库例程(routine)访问/修改一个内存中的全局结构或者位置.
  • 当这些线程调用此例程时都有可能会试图在同一时间试图修改这个全局结构体/内存位置.
  • 如果这个例程不采用某种同步构造来防止数据损坏,那么它就不是线程安全的.
使用外部库意味着:如果你不是有100%的把握这个例程是线程安全的,你就很有可能把自己置于问题之中.
建议:小心使用哪些没有明显保证线程安全的库或者对象.如果存在疑问,就假定他们不是线程安全的,直到证明为止. 
可以将调用不确定例程的调用“串行化”来达到目的,etc.

线程的局限性:

  • 虽然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_ 开头。一些例子如下所示:

前缀

函数组

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

保持数据的隐藏性(opaque)贯穿了API设计的始终。对于pthreads对象的创建或者修改可以通过调用创建,修改函数来完成。而对这些对象中包含的属性的修改,则有对应的属性处理函数。.
Pthreads API 大约包含了100个子例程。本文只侧重于其中的一部分,特别是那些开始pthreads编程的程序员可能马上就需要使用的。
为可移植性,应在每个使用 Pthreads 库的源文件中包括 pthread.h 头文件.
当前的 POSIX 标准是只为 C 语言定义的。Fortran 语言的程序员可以使用 C 函数调用的包装。一些 Fortran 编译器可能提供 Fortran pthreads.
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++