并行程序:有p个核,每个核独立于其他核并累加求和,以得到部分和 ,每个核的计算数为n/p
显然第二种方法更好!
并行程序的方法:任务并行和数据并行
任务并行:将待解决的任务所需要执行的各个任务分配到各个核上去执行
数据并行:将待处理的数据分配给各个核
2.1
在梯形图中从上到下容量依次递增,速率依次递减,每字节的成本依次递减
2.2
内存的数据被加载到Cache后,在某个时刻其要被写回内存,写内存有如下5种策略:写通(write-through)、写回(write-back)、写一次(write-once)、WC(write-combining)和UC(uncacheable)。
写通(write-through):
当cache写命中时,处理器对Cache写入的同时,将数据写入到内存中,内存的数据和Cache中的数据都是同步的,这种方式比较简单、可靠。但是处理每次对cache更新都需要对内存写操作,因此总线工作繁忙,内存的带宽被大大占用,因此运行速度会受到影响。
假设一段程序在频繁地修改一个局部变量,局部变量生存周期很短,而且其他进程/线程也用不到它,CPU依然会频繁地在Cache和内存之间交换数据,造成不必要的带宽损失。
当cache写未命中时,只有直接向主存写入了,但此时是否将修改过的主存块取到cache,写直达法却有两种选择。
一是取来并且为它分配一个位置,称为WTWA(Write–Through–with–Write–Allocate)。
另一种是不取称为WTNWA法(WriteThrough–with.NO-Write–Allocate)。
前 一种法保持了cache/主存的一致性,但操作复杂,而后一种方法操作简化,但命中率降低,内存的修改块只有在读未命中对cache 进行替换时,才有可能射到cache 。写通发保证了写cache与写主存同步进行,图中为写通法WTNWA法的流程图。
OpenMP是一个针对共享内存编程的API。
MP = multiprocessing(多处理)
启动一个进程,然后由进程启动这些线程。
线程共享启动它们的进程的大部分资源,如对标准输入和标准输出的访问,但每个线程有它自己
的栈和程序计数器
在parallel之前,程序只使用一个线程,而当程序开始执行时,进程开始启动。当程序达到parallel指令时,原来的线程继续执行,另外thread_count-1个线程被启动。
隐式路障:即完成代码块的线程将等待线程组中的所有其他线程返回
私有栈:每个线程有它自己的栈,所以一个执行Hello函数的线程将在函数中创建它自己的私有局部变量
乱序输出:注意因为标准输出被所有线程共享,所以每个线程都能够执行printf语句,打印它的线程编号和线程数。由于对标准输出的访问没有调度,因此线程打印它们结果的实际顺序不确定
_OPENMP
是否定义。如果定义了,则能够包含共享作用域:能够被线程组中所有线程访问的变
量。
在 main 函 数 中 声 明 的 变 量 ( a,b,n,global_result 和
thread_count)对于所有线程组中被parallel指令启动的线
程都可访问
私有作用域:只能被单个线程访问的变量。
在parallel指令中,所有变量的缺省作用域是共享的
在parallel for中,由其并行化的循环中,循环变量的缺省作用域是私有的。
OpenMP是“基于指令”的共享内存API。
OpenMP的头文件为omp.h
omp.h是由一组函数和宏库组成
stdlib.h头文件即standard library标准库头文件。
#pragma
# pragma omp parallel
:
parallel指令是用来表示之后的结构化代码块应该被多个线程并行执行
结构化代码块是一条C语言语句或者只有一个入口和出口的一组复合C语句
编写规范:
OpenMP中,预处理器指令以#pragma omp开头
# pragma omp parallel clause1 \
clause2 … clauseN new-line
#pragma omp parallel [clause…] new-line
Structured-block
#pragma omp parallel private(i,j)
其中parallel就是指令,private是子句,且指令后
的子句是可选的。
编译指导语句后面需要跟一个new-line(换行符)。然后跟着的是一个structured-block
当整个编译指导语句较长时,也可以分多行书写,用
C/C++的续行符“\”连接起来即可。如:
#progma omp parallel claused1 \
Claused2 … clausedN new-line
Structured-block
规约操作是一个二元操作 (such as addition or multiplication).
规约操作就是将一个相同的操作重复应用在一个序列上,得到一个结果。所有的中间结果和最终结果都存储在规约变量里
规约操作通过reduction指令实现:
for指令是用来将一个for循环分配到多个线程中执行
for指令的使用方式:
1.单独用在parallel语句的并行块中;
2.与parallel指令合起来形成parallel for指令
注意:for指令与parallel for指令
派生出一组线程来执行后面的结构化代码块
只能并行for语句
parallel 结合 for实例:
include <stdio.h>
#include
int main(int argc, char* argv[]){
int j=0;
#pragma omp parallel num_threads(4)
{
#pragma omp for
for(j=0;j<4;j++)
printf("j=%d,ThreadId=%d\n",j,omp_get_thread_num());
}
return 0;
}
如果二者不结合,从结果可以看出四次循环都在一个线程里执行,可见,for指令要与parallel指令结合起来使用.
#include
#include
int main(int argc, char* argv[]){
int j=0;
#pragma omp parallel num_threads(4)
{
#pragma omp for
for(j=0;j<4;j++)
printf("First:j=%d,ThreadId=%d\n",j,omp_get_thread_num());
#pragma omp for
for(j=0;j<4;j++)
printf("Second:j=%d,ThreadId=%d\n",j,omp_get_thread_num());
}
return 0;
}
for循环构造的一些限制如下:
变量index必须是整型或指针类型。
表达式start、end和incr必须是兼容类型。
表达式start、end和incr不能够在循环执行期间改变,也
就是迭代次数必须是确定的。
在循环执行期间,变量index只能够被for语句中的“增
量表达式”修改。
for语句中不能含有break。
出现在reduction子句中的参数不能出现在private
用private子句声明的私有变量的初始值在parallel块或parallel for块的开始处是未指定的,且在完成之后也是未指定的
用法如下:default(shared|none)
用来声明一个或多个变量是共享变量,
用法如下:shared(list)
注意的是:
在并行区域内使用共享变量时,如果存在写操作,必须对共享变量加以保护;
否则,不要轻易使用共享变量,尽量将共享变量的访问转化为私有变量的访问
for循环串行代码:
#include
int main(int argc, char *argv[]){
int i;
for (i=0; i<10; i++){
printf("i=%d\n",i);
}
printf("Finished.\n");
return 0;
}
并行代码:
#include
#include
#include
int main(int argc, char *argv[]){
int i;
//用来指定执行之后的代码块的线程数目
int thread_count = strtol(argv[1],NULL,10);
# pragma omp parallel for num_threads(thread_count)
for (i=0; i<10; i++){
printf("i=%d\n",i);
}
printf("Finished.\n");
return 0;
}
编译:gcc −g −Wall −fopenmp −o omp_example omp_example .c
执行:. /omp_example 4
4个线程执行
HelloWorld代码
当两个或更多的线程加到global_result中,可能会出现不可预期的结果
critical:
critical指令用在一段代码临界区之前,临界区在同一时间内只能有一个线程执行它,其他线程要执行临界区则需要排队来执行它。
#pragma omp critical[(name)] new-line
structured-block
# progma omp critical
global_result += my_result
三个点:OpenMP实现奇偶排序,循环调度,环境变量
double omp_get_wtime(void)
: 返回从某个特殊点所经过的时间,单位秒;(这个时间点在程序运行过程中必须是一致性)
OpenMP中,任务调度主要用于for循环的并行。当循环中每次迭代的计算量不等时,如果简单地给各个线程分配相同次数的迭代,则会造成各个线程计算负载不均衡,使得有些线程先执行完、有些后执行完,造成某些CPU核空闲,影响程序性能。
更好的方案是轮流分配线程的工作
OpenMP中,对for循环并行化的任务调度可使用schedule子句来实现。
schedule(
type的类型:
static:迭代能够在循环执行前
分配给线程系统,以轮转的方式将任务分配给线程;
dynamic 或 guided :迭代循环执行时
被分配给线程
runtime:调度在运行时
决定
export OMP_SCHEDULE="static,1"
chunksize参数是非法的
。auto :编译器和运行时系统决定调度方式
chunksize是一个正整数,是块中的迭代次数 ,一次分配一个线程几个数
guided>dynamic>static
定义:环境变量是能够被运行时系统所访问的命名值,即它们在程序的环境中是可得的。
使用export命令设置环境变量值 export OMP_DYNAMIC=TRUE
OpenMP的环境变量主要有四个:
OMP_DYNAMIC
OMP_NUM_THREADS
指定线程数目的方法:
1. 不指定,即默认为处理器的核数;
2. 使用库函数omp_set_num_threads(int num)
3. 在#parama omp parallel num_threads(num)
4. 使用环境变量
$gcc –fopenmp -o 1 1.c
$OMP_NUM_THREADS =4
$export OMP_NUM_THREADS//设置环境变量值
OMP_NESTED
OMP_SCHEDULE
schedule:调度任务实现分配给线程任务的不同划分方式
函数:
omp_get_wtime():计算OpenMP并行程序花费时间
omp_set_num_thread(): 设置线程的数量
生产者/消费者模型描述的是有一块缓冲区作为仓库,生产者可将产品放入仓库,消费者可以从仓库中取出产品,模型图如下所示:
生产者和消费者模型的另外一个应用:共享内存系统上实现多线程消息传递。
每一个线程有一个共享消息队列,当一个线程要向另外一个线程发送消息时,它将消息放入目标线程的消息队列中
一个线程接收消息时,只需从它的消息队列的头部取出消息。
每个线程交替发送和接收消息,用户需要指定每个线程发送消息的数目。
当一个线程发送完所有消息后,该线程不断接收消息直到其他所有的线程都已完成,此时所有线程都结束了。
注意:访问消息队列并将消息入队,可能是一个临界区
接收消息的临界区问题与发送消息有些不同。
如果只使用一个变量来存储队列大小,那么对该变量的操作会形成临界区(有冲突)
解决方案:
可以使用两个变量:enqueued和dequeued,那么队列中消
息的个数(队列的大小)就为queue_size = enqueued – dequeued
问题:一个或多个线程可能在其它线程之前完成它的队列分配。那么完成分配的线程可能会试图开始向那些还没有完成队列分配的线程发送消息,这将导致程序崩溃
。
必须确保任何一个线程都必须在所有线程都完成了队列分配后才开始发送消息
解决: 需要一个显式路障,使得当线程遇到路障时,它将被阻塞,直到组中所有的线程都达到这个路障。
用来解决临界区的问题的
critical指令
atomic指令:只能保护由一条C语言赋值语句所形成的临界区。#pragma omp atomic
许多处理器提供专门的装载-修改-存储指令
使用这种专门的指令而不使用保护临界区的通用结构,可以更高效地保护临界区。
1. 消息列表
2. 队尾指针
3. 队首指针
4. 入队消息的数目
5. 出队消息的数目
为了减少参数传递时复制的开销,最好使用指向结构体的指针数组来实现消息队列
一个数组,若其元素均为指针类型数据,称为指针数组。即指针数组中的每一个元素都相当于一个指针变量。
类型名 *数组名[数组长度]; 如int *p[4];
指令:
atomic指令: 实现互斥访问最快的方法。
critical指令:保护临界区,实现互斥访问。
函数:
void omp_init_lock(omp_lock_t*lock);//初始化锁
void omp_destroy_lock(omp_lock_t*lock)//销毁锁
void omp_set_lock(omp_lock_t*lock);//尝试获得锁
void omp_unset_lock(omp_lock_t*lock);//释放锁
$gcc -g -Wall -fopenmp -o outputdir filename.c
$./outputdir