Parallel Architecture Lecture7
Parallel Architecture Lecture7
多线程
共享存储器模型
所有线程都可访问相同的全局共享内存;
数据可公有可私有;
私有数据只能由拥有它的线程访问;
数据传输对程序员透明;
同步大多隐式发生。
单线程和多线程进程
线程被称为进程中的“轻量级进程”,这是因为它与传统的进程相比,具有更少的资源占用和管理开销。
浅蓝色部分是进程的地址空间,每个线程都不一定会访问哪块,所以是弯曲的。
线程
与其他线程并发运行的代码;
每个线程都是静态有序的指令序列。
Java线程支持
Java内置对多线程的支持,MyThread类继承了Thread,而MyThread实现Runnable。
Java的垃圾回收器其实也是一个低优先级的线程。
例
这是一段三个线程同时打印从1到5的序号的代码:
class myth extends Thread {
myth (String name) {
super(name);
}
public void run() {
for(int i=1;i<=5;i++) {
System.out.println("\t From "+getName()+": i= "+i);
}
System.out.println("Exit from "+getName());
}
}
class ThreadTest {
public static void main(String args[]) {
new myth("ThreadA").start();
new myth("ThreadB").start();
new myth("ThreadC").start();
}
}
POSIX线程(C++)
POSIX兼容系统标准线程库,支持线程创建和管理;
有5个基本pthread函数:
pthread_create // 创建新的子线程
pthread_join // 等待另一个线程终止
pthread_equal // 比较线程id,看是否来自同一个线程
pthread_self // 返回调用线程的id
pthread_exit // 终止
OpenMP Classic 前置概念
OpenMP的概念
OpenMP(Open specifications for multi-processing,多处理开放规范)是一种应用API,用于指导多线程共享内存并行性。
架构
① Application(应用程序)是使用OpenMP进行开发的程序,它包含了指令(Directives),这些指令用于指示编译器如何并行化代码。
② 当应用程序首次调用OpenMP功能时,运行时库初始化其内部数据结构和线程池,以准备执行并行任务;应用程序可能通过运行时库的同步原语来协调线程。例如,使用#pragma omp barrier
实现线程间同步。在程序结束或不再需要并行执行时,应用程序也可能调用相关API进行清理,运行时库负责释放资源和执行终结算法,以确保系统资源的正确回收。
③ Directive Compiler(指令编译器)负责解析OpenMP指令,将这些指令转换为多线程代码传递给运行时库(Runtime Library)。运行时库使用这些信息来管理并发执行的细节。应用程序通过OpenMP指令标识并行区域。编译后的程序在运行时触发运行时库的相应功能,以划分任务、分配线程以及监控执行。
④ User(用户)可以手动编辑Environment Variables(系统环境变量)。
⑤ Runtime Library(运行时库)运行时库是OpenMP的核心组件之一。它提供了线程管理、任务调度和同步等功能。运行时库帮助管理和协调由编译器生成的多线程代码。运行时库的种种配置也受环境变量约束。
⑥ 系统中的实际线程统一由运行时库调度。
特征
标准化
在各种共享内存的架构的平台中提供高级接口。
省事
仅3-4条指令就足以描述并行性。
易用性
增量并行化串行程序:逐步将一个串行程序转换为并行程序的过程,而不需要立即进行全部的并行化修改。
如果使用了增量并行,开发者就不需要从一开始就写出并行代码,而是可以先写出串行代码,后期再按需求选择并行化其中的哪一部分。
编程模型
设计目标
基于线程的并行性
多个共享内存的线程显式并行,为程序员提供对并行化的完全控制。
基于编译制导
通过嵌在源码中的编译器指令(以#pragma omp
开头)来指定计算动作。
嵌套的多元素支持
在并行结构内部可以包含其他的并行结构,比如子线程中可以有别的子线程。
动态线程
更改并行区的线程数,可以随时创建、收回。
Fork-Join 模型
模型描述
OpenMP程序开始时都只有一个主线程,主线程在并行结构的开头分叉(创建一组并行的线程),并行区结束后在并行结构的末尾重新组合成单一主线程:
代码
#include <omp.h>
void saxpy() {
float a, x[ARRAY_SZ], y[ARRAY_SZ];
// 开始并行区,创建线程分叉
#pragma omp parallel {
int id = omp_get_thread_num(); // 返回当前线程id
int nthrs = omp_get_num_threads(); // 返回并行区中的线程总数
for(int i=id; i<ARRAY_SZ; i+=nthrs) { // 以线程数作为步长是为了避免同一个位置被重复计算
y[i] = a * x[i] + y[i];
}
}
}
术语
Construct(结构)
是一条语句,由一个指令和随后的结构化块组成。
Directive(制导)
C/C++中的以#pragma omp
开头的标识符,该指令制定了程序的行为。
Structured block(结构化块)
结构化块是具有单个出入口的语句,被一组大括号包围,其实就是C++里的语句块。
Master thread(主线程)
进入并行区时,创建其他线程组的线程。
Team(线程组)
在执行一个结构时进行协作的一个或多个线程。
Parallel region(并行区域)
绑定到OpenMP并行结构的,并可以由多个线程执行的语句或语句块。
Serial region(串行区域)
仅由主线程在任何并行区域的动态范围之外执行的语句。
Private(私有的)
被private命名的存储块只能由单个线程访问。
Shared(共享的)
被shared命名的存储块可以由多个线程访问。
OpenMP Classic 代码部分
并行域
短指令
例如:
#pragma omp parallel default(shared) private(beta, pi)
{
...
}
其中:
#pragma omp parallel
启动一个并行区;
default(shared)
指定并行区内的变量默认是共享的;
private(beta, pi)
把特定的两个变量beta
和pi
设定为私有。
clause
是子句,具体见下边。
长指令
子句类型:
if(scalar_expression)
条件并行。只有当条件表达式为真时,才会执行并行化。如果表达式为假,则该区域会串行执行。
private(list)
指定列表中的变量为私有变量。
shared(list)
指定列表中的变量为共享变量。
default(shared|none)
设定所有未明确声明的变量默认是共享还是无定义。选择none
时必须明确声明每个变量是共享还是私有的。
firstprivate(list)
指定变量的初始值为串行区域的值,但每个线程都有自己独立的副本。
reduction(operator: list)
进行归约计算。每个线程对列表中的变量执行运算符操作,并在并行区域结束时合并结果。常用于求和、乘积等累加操作,主要目的是为了避免写冲突(数据竞争),并确保多个线程对一个共享变量进行累加、乘积等操作时得到正确的结果。
copyin(list)
将主线程中某些线程私有变量的值复制给其他线程。常用于传递初始值。
例如以下代码段:
#include <omp.h>
#include <iostream>
void example() {
int n = 10;
int total = 0;
int shared_var = 100;
int private_var = 0;
// 启动并行区域
#pragma omp parallel num_threads(4) if(n > 5) default(none) shared(shared_var) private(private_var) reduction(+: total)
// n>5,所以下边的语句块并行执行;没有默认类型,所以后面要指定所有变量访问权限;在最后加总所有变量并赋给total。
{
int id = omp_get_thread_num(); // 获取线程ID
private_var = id * 2; // 每个线程有独立的 private_var
#pragma omp for // 被这个命令附着的语句会被分配给所有线程执行
// 注意,即使用omp for,也必须在头部声明reduction的变量和操作,这是为了避免写冲突,即被reduction声明的变量是收到互斥锁保护的。
for (int i = 0; i < n; ++i) {
total += i; // 使用reduction对子句,在所有线程上累加 i 的值到 total
}
#pragma omp critical
std::cout << "Thread " << id << " with private_var: " << private_var << "\n";
}
std::cout << "Total sum: " << total << "\n";
}
例:利用并行域计算 的值
计算机算
#include <omp.h>
static long num_steps = 100;
double step; // 步长,微分宽度
#define NUM_THREADS 16
void main () {
int i;
double x, pi, sum[NUM_THREADS];
step = 1.0/(double) num_steps;
omp_set_num_threads(NUM_THREADS); // 设置线程数
#pragma omp parallel
{
double x;
int id;
int i;
id = omp_get_thread_num();
printf("this is thread %d.\n",id);
for (i=id, sum[id]=0.0;i< num_steps; i=i+NUM_THREADS) {
// 并行区以线程数为步长循环,避免重复计算
x = (i+0.5)*step; // +0.5是为了计算每个小矩形中心的值
sum[id] += 4.0/(1.0+x*x); // 当前x的函数值
}
}
for(i=0, pi=0.0;i<NUM_THREADS;i++)
pi += sum[i] * step; // 每个小矩形的面积加出积分值
printf("Program successfully terminated!\n");
printf("PI is %lf.\n",pi);
}
工作共享结构
特点
在成员线程之间划分区域执行;
在结构末尾有一个隐含的同步屏障;
在结构开头(进入时)没有同步障碍;
这种结构不会启动新的线程。
结构类型
#pragma omp for
在整个团队(线程组)中共享一个循环迭代,被这个命令附着的语句会被分配给所有线程执行。
#pragma omp single
把一段代码序列化,确保该代码段仅由一个线程执行。
#pragma omp section
把工作分成独立部分,每个部分由一个线程执行,可用于实现功能并行。
#pragma omp for
原理
紧随这个指令的循环必须由所有线程并行执行。
子句类型
schedule (type [,chunk])
决定如何将循环迭代分配给线程。type
可以是以下几种:
static
: 静态分配。循环迭代会分成固定大小的块,按顺序分配给线程;
dynamic
: 动态分配。线程完成一块后,从队列中动态地获取下一块,适合负载不均的情况;
guided
: 分配的块大小从大到小逐步减少,适合负载不均匀但逐渐减少的情况;
auto
: 让编译器决定分配策略;
runtime
: 使用运行时环境变量 OMP_SCHEDULE
设置的调度类型。
不同type
下的循环调度分配情况:
分配在4个线程上的500轮循环:
可见,static
下,所有线程分到的任务是连续且同样大小的;
dynamic
下,线程在做完一次任务后自己拿下一块没人做的;
guided
下,每个进程分配的也都是差不多大小的块,但是逐渐减少。
Question
这里我没太理解guided
到底能干嘛用?是静态的吗?
ordered
保证循环迭代按顺序执行。用于处理每个线程完成的结果需要按顺序合并的情况。当使用 ordered
子句时,循环内必须包含 #pragma omp ordered
来指定顺序区域。
private (list)
将 list
中的变量声明为私有变量。每个线程有各自的副本,修改不会影响其他线程。
这些变量在进入循环时没有初始值,并在循环结束后丢弃。
firstprivate (list)
类似于 private
,但在进入循环时,每个线程的私有变量都初始化为其全局变量的值(带初值的私有变量)。
lastprivate (list)
在循环结束时,将最后一个迭代中计算得到的私有变量的值复制到对应的全局变量中(保留出循环时的值)。
nowait
指定线程在完成自己的循环迭代后不需要等待其他线程,可以直接继续执行后续代码。如果加了这一句,并行区最后就没有了隐式的同步障。
例:利用工作共享结构计算 的值
其实就是在for循环外加上#pragma omp for
指令:
#include <omp.h>
static long num_steps = 100;
double step;
#define NUM_THREADS 16
void main () {
int i;
double x, pi, sum[NUM_THREADS];
step = 1.0/(double) num_steps;
omp_set_num_threads(NUM_THREADS);
#pragma omp parallel
{
double x;
int id;
int i;
id = omp_get_thread_num();
printf("this is thread %d.\n",id);
#pragma omp for
for (i=0; i<num_steps; i++) {
// 如果加了for指令,就不用在循环条件中手动指定每个线程负责哪部分
x = (i+0.5)*step;
sum[id] += 4.0/(1.0+x*x);
}
}
for(i=0, pi=0.0;i<NUM_THREADS;i++)
pi += sum[i] * step; // 每个小矩形的面积加出积分值
printf("Program successfully terminated!\n");
printf("PI is %lf.\n",pi);
}
组合的平行共享任务结构
结构类型
#pragma omp parallel for
#pragma omp parallel
和#pragma omp for
的简写。
#pragma omp parallel sections
#pragma omp parallel
和#pragma omp section
的简写。
例
#include <omp.h>
#define N 1000
#define CHUNKSIZE 100
main () {
int i, chunk;
float a[N], b[N], c[N];
/* Some initializations */
for (i=0; i < N; i++)
a[i] = b[i] = i * 1.0;
chunk = CHUNKSIZE;
#pragma omp parallel for shared(a,b,c,chunk) private(i) schedule(static,chunk)
for (i=0; i < n; i++)
c[i] = a[i] + b[i];
}
子句的用法还是一样。
同步结构
屏障机制
每个并行结构的开始和结束会有隐式屏障,确保所有线程都在同一“起点”和“终点”,所有其他控制结构的结尾也会有隐式屏障。可以通过使用 nowait
子句移除隐式同步,避免不必要的等待,提高效率。
显式同步结构
#pragma omp critical
用于保护代码块,使其在每次只有一个线程可以执行,以防止竞态条件。
竞态条件是说,在第一个线程退出critical
区域之前,其他线程将被阻塞。
例:
用了这条指令之后,
cnt = 0;
f = 7;
#pragma omp parallel
{
#pragma omp for
for (i=0; i<20; i++) {
if (b[i] == 0) {
#pragma omp critical
cnt ++;
}
a[i] = b[i] + f * (i+1);
}
}
可见,在两条红线之间,即同一时间内,只有单个的线程在操作共享变量cnt
,避免了写冲突。
#pragma omp master
只允许主线程执行其后的代码块。
#pragma omp barrier
所有线程在此等候,直到每个线程都到达该点后才继续执行(手动设置屏障,在每个并行块的末尾其实都有个这东西)。
#pragma omp atomic
用于确保对某个共享变量的原子操作,防止数据竞争。
#pragma omp flush
用于强制对变量的内存更新,使所有线程看到一致的变量状态。
#pragma omp ordered
确保在一个有序的并行循环中,执行代码块的顺序与迭代顺序一致。
数据环境(权限)
数据范围
数据范围属性子句
用来明确定义变量的作用域,默认情况下大部分变量都是共享的。
全局变量
文件作用域的变量、静态变量。
私有变量
循环索引(i
);堆栈从并行区域调用的子程序中的变量。
语句
就是上面说的那些#pragma omp shared
,#pragma omp lastprivate
等东西。
环境变量
OMP_SCHEDULE
仅适用于for指令,如果for指令把调度子句schedule (type [,chunk])
中的type
设置为Runtime
(也就是从环境变量中获取)时,可以用这个变量设定里面的参数:
setenv OMP_SCHEDULE "guided, 4"
// 这个环境变量让for语句使用guided调度,每个线程最小分配4次迭代
setenv OMP_SCHEDULE "dynamic"
// 动态分配迭代,线程完成任务后获取新的迭代任务
OMP_NUM_THREADS
设置执行时使用的最大线程数。
OMP_DYNAMIC
启用或禁用执行并行区域时线程数的动态调整。
OMP_NESTED
启用或禁用嵌套并行。