并行编程是一种编程范式,它通过同时执行多个进程或线程来利用计算机系统的多个处理器或核心的能力。这种方法可以显著提高程序的执行效率和性能,特别是对于计算密集型任务和数据处理任务。随着多核处理器的普及,并行编程变得越来越重要。
并行编程基础概念
1. 并行与并发的区别
-
并发:任务在时间上重叠执行(不一定是同时)
-
并行:任务真正同时执行(需要多核/多处理器支持)
2. 关键术语
-
粒度:任务划分的大小(细粒度 vs 粗粒度)
-
负载均衡:各处理单元工作量均衡分配
-
同步:协调并行执行的任务
-
通信开销:处理单元间数据交换的成本
并行计算模型
1. 共享内存模型
-
特点:所有处理器共享同一内存空间
-
实现技术:
-
多线程(Pthreads, std::thread)
-
OpenMP(指令式并行)
-
Intel TBB(任务并行库)
-
2. 分布式内存模型
-
特点:每个处理器有独立内存,通过消息传递通信
-
实现技术:
-
MPI(消息传递接口)
-
PVM(较老的并行虚拟机)
-
3. 数据并行模型
-
特点:相同操作应用于不同数据
-
实现技术:
-
SIMD指令(CPU向量化)
-
GPU编程(CUDA/OpenCL)
-
OpenACC(指令式GPU编程)
-
4. 任务并行模型
-
特点:不同任务并行执行
-
实现技术:
-
线程池
-
任务调度系统
-
模型对比
特性 | 共享内存 | 分布式内存 | GPU编程 |
---|---|---|---|
编程复杂度 | 低 | 中 | 高 |
可扩展性 | 单节点(10-100核) | 超大规模(百万进程) | 单设备(数千核心) |
内存访问 | 统一地址空间 | 显式消息传递 | 分离内存空间 |
最佳适用场景 | 细粒度任务并行 | 粗粒度数据并行 | 大规模数据并行 |
典型延迟 | 纳秒级 | 微秒级 | 微秒级(主机-设备) |
带宽 | 高(100+GB/s) | 依赖网络(1-100GB/s) | 极高(500+GB/s设备内存) |
并行编程关键技术
1. 同步机制
-
锁:互斥锁、读写锁、自旋锁
-
原子操作:无锁编程基础
-
屏障:所有线程到达同步点
-
条件变量:线程间事件通知
2. 通信模式
-
点对点通信:send/recv
-
集合通信:broadcast, gather, reduce
-
单向通信:put/get(PGAS模型)
3. 并行模式
-
MapReduce:大数据处理
-
Pipeline:流水线并行
-
Divide-and-Conquer:分治策略
现代并行编程框架
1. CPU并行框架
-
OpenMP:共享内存并行
-
MPI:分布式内存并行
-
C++17并行算法:标准库并行化
2. GPU并行框架
-
CUDA:NVIDIA GPU编程
-
OpenCL:跨平台异构计算
-
SYCL:基于C++的单源异构编程
-
内存模型: 主机(CPU)和设备(GPU)内存分离
-
执行模型: 大规模细粒度并行
-
硬件基础: GPU加速器
-
适用场景: 计算密集型, 数据并行任务
3. 高级抽象框架
-
Kokkos:性能可移植性框架
-
RAJA:循环并行抽象
-
OneAPI:Intel统一编程接口
共享内存编程:多线程
共享内存编程是多线程编程中的核心概念,它允许多个线程访问和修改同一块内存区域。下面我将介绍多线程共享内存编程的关键方面:
基本概念
-
共享内存:多个线程可以直接访问同一进程地址空间中的变量和数据结构
-
线程私有内存:每个线程也有自己的栈空间,局部变量通常存储在这里
共享内存的优势
-
通信效率高(无需数据拷贝)
-
实现简单(直接访问内存)
-
适合紧密耦合的并行任务
主要挑战:竞态条件
当多个线程同时访问共享数据且至少有一个线程在修改数据时,可能会出现竞态条件(Race Condition),导致不确定的结果。
同步机制
1. 互斥锁 (Mutex)
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;
void* thread_func(void* arg) {
pthread_mutex_lock(&lock);
// 临界区开始
shared_data++;
// 临界区结束
pthread_mutex_unlock(&lock);
return NULL;
}
2. 读写锁 (Read-Write Lock)
#include <pthread.h>
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读线程
pthread_rwlock_rdlock(&rwlock);
// 读取共享数据
pthread_rwlock_unlock(&rwlock);
// 写线程
pthread_rwlock_wrlock(&rwlock);
// 修改共享数据
pthread_rwlock_unlock(&rwlock);
3. 条件变量 (Condition Variables)
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
// 等待线程
pthread_mutex_lock(&mutex);
while (!ready) {
pthread_cond_wait(&cond, &mutex);
}
// 处理数据
pthread_mutex_unlock(&mutex);
// 通知线程
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
原子操作
现代处理器和编程语言提供了原子操作,可以在不加锁的情况下安全地执行简单操作:
#include <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
void increment() {
atomic_fetch_add(&counter, 1);
}
最佳实践
-
最小化共享:尽可能减少共享数据的使用
-
细粒度锁定:使用多个锁保护不同的数据,减少争用
-
避免死锁:按固定顺序获取多个锁
-
优先使用高级抽象:如线程安全容器、并行算法库
-
使用线程局部存储:对于不需要共享的数据
常见问题
-
虚假共享(False Sharing):不同CPU核心频繁修改同一缓存行的不同变量,导致性能下降
-
解决方案:填充数据结构使变量位于不同缓存行
-
-
活锁(Livelock):线程不断尝试获取资源但始终失败
-
解决方案:引入随机退避机制
-
-
优先级反转:高优先级线程等待低优先级线程持有的锁
-
解决方案:使用优先级继承协议
-
多线程共享内存编程是强大的工具,但也需要谨慎使用以避免并发问题。
OpenMP 共享内存编程
OpenMP 是一种广泛使用的共享内存并行编程 API,它通过编译器指令(pragma)简化了多线程程序的开发。以下是 OpenMP 的核心概念和使用方法:
基本结构
#include <omp.h>
int main() {
// 串行代码
#pragma omp parallel
{
// 并行区域
int thread_id = omp_get_thread_num();
printf("Hello from thread %d\n", thread_id);
}
// 串行代码
return 0;
}
主要特性
1. 并行区域
#pragma omp parallel
创建一组线程执行代码块
2. 工作共享结构
-
for循环并行化:
#pragma omp parallel for
for (int i = 0; i < N; i++) {
a[i] = b[i] + c[i];
}
-
sections分配:
#pragma omp parallel sections
{
#pragma omp section
{ /* 任务1 */ }
#pragma omp section
{ /* 任务2 */ }
}
-
单线程执行:
#pragma omp single
{
// 只有一个线程执行这部分代码
}
3. 数据共享属性
-
shared
:变量在所有线程间共享 -
private
:每个线程有自己的副本 -
firstprivate
:private且用原值初始化 -
lastprivate
:private且最后的值传回主线程 -
reduction
:归约操作
int sum = 0; #pragma omp parallel for reduction(+:sum) for (int i = 0; i < N; i++) { sum += a[i]; }
4. 同步机制
-
屏障同步:
#pragma omp barrier
-
临界区:
#pragma omp critical
{
// 一次只有一个线程执行
}
-
原子操作:
#pragma omp atomic counter++;
-
锁:
omp_lock_t lock;
omp_init_lock(&lock);
#pragma omp parallel
{
omp_set_lock(&lock);
// 临界区
omp_unset_lock(&lock);
}
omp_destroy_lock(&lock);
环境控制
设置线程数:
omp_set_num_threads(4);或通过环境变量:
export OMP_NUM_THREADS=4
获取线程信息:
int num_threads = omp_get_num_threads(); int thread_id = omp_get_thread_num(); int max_threads = omp_get_max_threads();
高级特性
1. 任务 (Tasks)
#pragma omp parallel
{
#pragma omp single
{
for (int i = 0; i < N; i++) {
#pragma omp task
{
process(i);
}
}
}
}
2. 嵌套并行
omp_set_nested(1); // 启用嵌套并行
#pragma omp parallel num_threads(2)
{
#pragma omp parallel num_threads(2)
{
// 嵌套并行区域
}
}
3. SIMD 指令
#pragma omp simd for (int i = 0; i < N; i++) { a[i] = b[i] + c[i]; }
性能考虑
-
负载均衡:使用
schedule
子句#pragma omp parallel for schedule(dynamic, chunk_size)
-
避免虚假共享:合理安排数据布局
-
减少同步开销:最小化临界区
-
线程创建开销:重用并行区域
示例:矩阵乘法
void matrix_multiply(int N, float A[N][N], float B[N][N], float C[N][N]) {
#pragma omp parallel for
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
C[i][j] = 0;
for (int k = 0; k < N; k++) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
}
OpenMP 提供了一种简单而强大的方式来开发共享内存并行程序,特别适合循环级并行化和任务并行化应用。
分布式内存编程:MPI
MPI (Message Passing Interface) 是分布式内存系统上进行并行编程的标准接口,适用于集群和超级计算机环境。与共享内存模型不同,MPI 进程拥有各自独立的内存空间,通过消息传递进行通信。
MPI 基础概念
1. 基本结构
#include <mpi.h>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv); // 初始化MPI环境
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程ID
MPI_Comm_size(MPI_COMM_WORLD, &size); // 获取总进程数
printf("Hello from process %d of %d\n", rank, size);
MPI_Finalize(); // 终止MPI环境
return 0;
}
点对点通信
1. 基本发送与接收
int data;
if (rank == 0) {
data = 100;
MPI_Send(&data, 1, MPI_INT, 1, 0, MPI_COMM_WORLD);
} else if (rank == 1) {
MPI_Recv(&data, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
printf("Received %d\n", data);
}
2. 通信模式
-
标准模式:
MPI_Send
(可能缓冲或阻塞) -
缓冲模式:
MPI_Bsend
(必须预先提供缓冲区) -
同步模式:
MPI_Ssend
(接收方准备好才开始发送) -
就绪模式:
MPI_Rsend
(确保接收方已发布接收)
3. 非阻塞通信
MPI_Request request;
MPI_Isend(&data, 1, MPI_INT, 1, 0, MPI_COMM_WORLD, &request);
// 可以在这里做其他工作
MPI_Wait(&request, MPI_STATUS_IGNORE); // 等待通信完成
集体通信
1. 广播 (Broadcast)
int data;
if (rank == 0) {
data = 100;
}
MPI_Bcast(&data, 1, MPI_INT, 0, MPI_COMM_WORLD);
2. 收集 (Gather)
int local_data = rank;
int* gathered_data = NULL;
if (rank == 0) {
gathered_data = (int*)malloc(size * sizeof(int));
}
MPI_Gather(&local_data, 1, MPI_INT,
gathered_data, 1, MPI_INT,
0, MPI_COMM_WORLD);
3. 分散 (Scatter)
int* data_to_scatter = NULL;
int received_data;
if (rank == 0) {
data_to_scatter = (int*)malloc(size * sizeof(int));
// 初始化数据...
}
MPI_Scatter(data_to_scatter, 1, MPI_INT,
&received_data, 1, MPI_INT,
0, MPI_COMM_WORLD);
4. 归约 (Reduce)
int local_value = rank;
int global_sum;
MPI_Reduce(&local_value, &global_sum, 1, MPI_INT,
MPI_SUM, 0, MPI_COMM_WORLD);
5. 全归约 (Allreduce)
int local_value = rank;
int global_sum;
MPI_Allreduce(&local_value, &global_sum, 1, MPI_INT,
MPI_SUM, MPI_COMM_WORLD);
派生数据类型
处理非连续数据:
MPI_Datatype vector_type;
MPI_Type_vector(10, 1, 5, MPI_INT, &vector_type); // 10个块,每块1个元素,步长5
MPI_Type_commit(&vector_type);
// 使用新类型发送数据
MPI_Send(buffer, 1, vector_type, dest, tag, MPI_COMM_WORLD);
MPI_Type_free(&vector_type);
进程组与通信域
1. 创建新通信域
MPI_Comm new_comm;
int color = rank % 2; // 按奇偶分组
MPI_Comm_split(MPI_COMM_WORLD, color, rank, &new_comm);
int new_rank;
MPI_Comm_rank(new_comm, &new_rank);
2. 进程拓扑
int dims[2] = {2, 2}; // 2x2网格
int periods[2] = {0, 0}; // 非周期性边界
MPI_Comm cart_comm;
MPI_Cart_create(MPI_COMM_WORLD, 2, dims, periods, 0, &cart_comm);
int coords[2];
MPI_Cart_coords(cart_comm, rank, 2, coords);
性能优化技巧
-
重叠计算与通信:使用非阻塞通信
-
批量发送:减少小消息数量
-
使用合适的集合操作:避免多个点对点通信
-
负载均衡:合理分配计算任务
-
避免死锁:注意通信顺序
示例:并行矩阵乘法
void parallel_matrix_multiply(int N, float* A, float* B, float* C) {
int rows_per_proc = N / size;
float* local_A = (float*)malloc(rows_per_proc * N * sizeof(float));
float* local_C = (float*)malloc(rows_per_proc * N * sizeof(float));
// 分发矩阵A的行
MPI_Scatter(A, rows_per_proc*N, MPI_FLOAT,
local_A, rows_per_proc*N, MPI_FLOAT,
0, MPI_COMM_WORLD);
// 广播整个矩阵B
MPI_Bcast(B, N*N, MPI_FLOAT, 0, MPI_COMM_WORLD);
// 局部计算
for (int i = 0; i < rows_per_proc; i++) {
for (int j = 0; j < N; j++) {
local_C[i*N+j] = 0;
for (int k = 0; k < N; k++) {
local_C[i*N+j] += local_A[i*N+k] * B[k*N+j];
}
}
}
// 收集结果
MPI_Gather(local_C, rows_per_proc*N, MPI_FLOAT,
C, rows_per_proc*N, MPI_FLOAT,
0, MPI_COMM_WORLD);
free(local_A);
free(local_C);
}
混合编程模型
MPI 可以与 OpenMP 结合使用,形成混合并行编程模型:
#include <mpi.h>
#include <omp.h>
int main(int argc, char** argv) {
MPI_Init(&argc, &argv);
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &size);
#pragma omp parallel
{
int thread_id = omp_get_thread_num();
printf("MPI rank %d, thread %d\n", rank, thread_id);
}
MPI_Finalize();
return 0;
}
MPI 是高性能计算中最常用的编程模型之一,特别适合大规模科学计算和数据处理应用。它的主要优势在于可扩展性强,能够在数千甚至数百万个处理器核心上高效运行。