并行计算概述
并行计算基本概念
| 基本概念 | 内容 |
|---|---|
| 计算 | 数学计算、数据处理、IT服务 |
| 性能 | FLOPS(浮点计算数/秒) |
| 浮点计算能力 | 计算机主频每时钟周期的浮点指令数 |
| 产生原因 | 满足不断增长的计算力需求:用多个处理器同时解决一个问题 计算机硬件与网络技术的发展 单处理器性能提升受限 存储、I/O速率远低于处理器 |
并行化方法
域分解
首先确定数据如何划分到各个处理器,然后确定每个处理器所需要做的事情。

任务分解
首先将任务划分到各个处理器,然后确定各个处理器需要处理的数据。

流水线
将一个任务拆分成多个步骤,通过不同步骤在时间上的重叠达到“并行”的效果。
并行计算硬件环境
PRAM模型
PRAM全称为Parallel Random Access Machine,它具备以下特点:
- 每个处理器可同时从共享的内存中读取数据到自己的寄存器
- 每个处理器执行计算过程,数据存储在本地寄存器
- 每个处理器可同时将数据写入到共享的内存(存在潜在冲突)

PRAM模型
PRAM模型可以衍生出以下模型:
模型 特点 EREW 任意两个处理器不能并发读,也不能并发写 CREW 可以并发读,但不能并发写 CRCW 可以并发读,也可以并发写
根据PRAM模型,可以对前缀求和 作出并行算法设计:

处理器
SIMD体系
SIMD是一条指令同时操作多个数据,适合数据级并行的计算机体系结构。
Flynn分类法(Flynn’s Taxonomy)是计算机体系结构领域最经典的分类方法之一。它根据指令流(Instruction Stream)——机器执行的指令序列,和数据流(Data Stream)——由指令流调用的数据序列两个维度的"单"(Single)或"多"(Multiple)组合,将计算机体系结构分为四类:
类型 全称 指令流 数据流 典型代表 特征 SISD Single Instruction Single Data 单 单 早期单核CPU(如Intel 8086)、传统冯·诺依曼架构 顺序执行,一次处理一个任务的一> 份数据 SIMD Single Instruction Multiple Data 单 多 GPU、向量处理器、Intel SSE/AVX指令集 一条指令同时操作多个数据,适合数据级并行 MISD Multiple Instruction Single Data 多 单 极少见,主要用于容错系统(如航天器冗余控制) 多个指令同时处理同一数据,理论意义大于实际 MIMD Multiple Instruction Multiple Data 多 多 多核CPU、分布式计算集群、高性能服务器 多个独立指令流处理多个数据流,实现任务级并行
多核处理器
进入21世纪,曾预言“CPU主频18个月翻一番”的摩尔定律不再适用于当下处理器的发展方向。
限制单核性能主要有以下几个方面:
- 功耗墙:CPU越小,单位面积产生的热量越多,散热越困难。
- 存储墙:CPU缓存占据了70%以上的芯片面积,限制了性能的进一步提升。
多核处理器的特点:
- 多个复杂度适中,相对低功耗的处理核心并行工作。
- CPU时钟频率基本不变、
GPU
GPU将更多元件用于数据处理,而非控制和存储。
可以将GPU视作超大规模并行协处理器和SPMD(单程序多数据)模式,实现了数据并行。
互连网络
静态互连网络:处理单元之间有固定连接的一类网络,在程序执行期间这种点到点的链接保持不变:
| 分类 | 特点 |
|---|---|
| 一维线性阵列 | 每个节点只与其左右近邻相连($N$个节点用$N-1$条边串接) |
| 二维网孔 | 每个节点只与其上、下、左、右的近邻相连(边界结点除外) |
| 二叉树 | 与完全二叉树的结构类似 |
| 超立方 | 更为复杂的空间结构 |
动态网络:用交换开关构成的,可按应用程序的要求动态地改变连接组态。包括总线、交叉开关等
内存访问模式
并行计算系统的体系结构
| 体系结构 | 特点 |
|---|---|
| PVP(Parallel Vector Processor) | 含有为数不多、功能强大的定制向量处理器 |
| SMP(Symmetric Multiprocessor) | 多个处理器通过总线或交叉开关连接到共享存储器 |
| MMP(Massively Parallel Processor) | 处理节点采用微处理器,系统中有物理上的分布式存储器 |
多线程并行程序设计
多线程基本概念
线程(Thread)是进程上下文(contex)中执行的代码序列,也称作轻量级进程。
在支持多线程的系统中,进程是资源分配的实体,线程是被调度执行的基本单元。
| 线程与进程的比较 | 内容 |
|---|---|
| 调度 | 线程是CPU调度的基本单位,进程是资源拥有的基本单位。同一进程中线程的切换不会引起进程切换,从而避免频繁的系统调用。 |
| 并发性 | 不仅进程之间可以并发执行,一个进程的多个线程之间也可以并发执行,一个进程下可设置多个线程 |
| 拥有资源 | 进程是拥有资源的独立单位;线程不拥有系统资源,但可以访问其所属进程的资源,即一个进程的资源可供其所有线程共享。 |
| 系统开销 | 进程在创建或销毁时系统均需要为之分配或回收资源,进程切换时系统需保存当前进程的所有设置;线程切换时只需保存和设置少量寄存器的内容,同一进程的线程之间的通信比较容易。 |
在计算机中,系统通过线程池管理线程。一个线程池可维护多个线程,等待调度器分配可并发执行的任务。避免了在短处理时间任务时创建与销毁线程的代价。
共享存储访问
计算机采用层次结构存储系统。

竞态条件与临界区
但是这样会出现竞态条件(Race Conditions):当两个或多个线程试图在同一时刻访问共享内存或读写某些共享数据时,寄存器的值可能无法及时更新,导致最后的结果取决于线程的执行顺序。
为了解决这一困境,提出了临界区的概念:包含访问共享数据的代码。因此,在临界区内,任意时刻至多只能有一个线程在执行相关代码。
互斥锁
互斥锁(mutex)是实现线程同步的一种方法。线程对共享资源访问之前必须先获得锁,否则线程将保持等待状态,直到该锁可用。
实例分析
假设现有一段长文本,要求统计其中3的个数
普通的串行代码应为:
1int *array;
2int length;
3int count;
4
5int countThree() {
6 int i;
7 count = 0;
8 for (i=0; i<length; i++) {
9 if (array[i] == 3) {
10 count++;
11 }
12 }
13 return count;
14}
运用域分解并行化方法,将其划分为若干个子数据段:
1int t;
2int *array;
3int length;
4int count;
5
6int countThree() {
7 int i;
8 count = 0;
9 // 创建线程
10 for (i=0; i<t; i++) {
11 thread_create(countThree_thread, i);
12 }
13 return count;
14}
15
16void countThree_thread(int id) {
17 int length_per_thread = length / t;
18 int start = id*length_per_thread;
19
20 for (i = start; i<start+length_per_thread; i++) {
21 if (array[i] == 3) {
22 // 加互斥锁
23 mutex_lock(m);
24 count ++;
25 // 解锁
26 mutex_unlock(m);
27 }
28 }
29}
但是,此并行算法的性能远不及串行算法:

这是因为加锁会消耗时间,同时所有线程两两互斥可看作串行逻辑,并没有变化。
改进方法:加锁过程应当放在循环计数外。当每个线程分别计数完成后,再将各个数目相加得到总数目。
1...
2int private_count[MaxThreads];
3mutex m;
4
5void countThree_thread(int id) {
6 int length_per_thread = length / t;
7 int start = id*length_per_thread;
8
9 for (i = start; i<start+length_per_thread; i++) {
10 if (array[i] == 3) {
11 private_count[id]++;
12 }
13 }
14 // 汇总时必须加锁,防止出现竞争
15 mutex_lock(m);
16 count += private_count[id];
17 mutex_unlock(m);
18}
修改后并行效率大幅提升,但仍与串行有差距。这是因为计算机系统的cache一致性与伪共享,虽然不同线程的private_count形式上互不干扰,但在缓存中可能处于一个缓存块中,在更新数据时会发生阻塞。

一种解决办法是强行将计数器的占用加大,使它们分别位于不同的缓存块中。
PThread多线段
主要操作函数:
| 函数 | 功能 |
|---|---|
pthread_create |
创建一个线程 |
pthread_cancel |
终止另一个线程 |
pthread_detach |
分离线程 |
pthread_equal |
检查两个线程的id是否相等 |
pthread_exit |
退出线程但不退出进程 |
pthread_join |
等待一个线程 |
pthread_self |
获得自己的线程id |
Java多线程
创建方法:
- 通过
Thread类的子类实现多线程。 - 定义一个实现
Runnable接口的类实现多线程。