目录
- OpenMP并行运行结构图
- 句式
- parallel制导命令
- 隐式同步
- parallel的for命令
- parallel的for命令
- 静态调度
- dynamic参数
- guided参数
- sections制导指令
- single制导指令
- 解决多线程竞争
- 临界区
- 矩阵所有元素+1
- 任务池
- 同步点
- shared和private
- 单语句原子操作#pragma omp atomic
- 复杂样例程序
OpenMP并行运行结构图
句式
在C/C++程序中,OpenMP的所有的编译指导命令都是以#pragma omp开始的,后面跟具体的功能指导命令,命令形式如下:
#pragma omp 指令 子句,子句,子句……
指令可以单独出现,子句必须出现在指令之后。
parallel制导命令
parallel制导命令表示接下来由花括号括起来的区域将创建多个线程并行执行。可以用num_threads子句来控制线程的个数,如下:
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
#pragma omp parallel num_threads(5)
{
cout << "Hello, world!" << endl;
}
}
也可以用此:
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
omp_set_num_threads(2);
#pragma omp parallel
{
cout << "Hello, world!" << endl;
}
}
omp_set_num_threads(num_threads)函数设置的是整个程序中的线程数,其作用域覆盖从调用点开始到程序结束的整个范围。也就是说,一旦设置了总线程数,它将适用于该调用点之后的所有并行区域。
然而,需要注意的是,设置总线程数并不保证在整个程序执行期间都保持不变。比如,在并行区域的内部或者其他地方,通过调用 omp_set_num_threads() 来改变总线程数,会覆盖之前的设置并应用新的值。
另外,如果在程序中存在多个 omp_set_num_threads() 函数调用,后面的调用会覆盖之前的设置,并且新的值会在之后的并行区域生效。
综上所述,omp_set_num_threads() 函数的作用域在调用点及其之后的整个程序执行期间,并且可以在程序的不同位置进行多次调用以更改总线程数。
隐式同步
在 #pragma omp parallel 域中,所有的线程会同时开始执行并行区域内的代码。当线程到达某个同步点时(通常是并行区域的结束),它们会自动停止在那个位置进行等待和同步,直到所有线程都到达同步点为止。
这种默认的同步行为可以确保并行区域中的所有线程在继续执行后续代码之前,都能够达到同一个状态。这样就避免了线程之间的竞争问题,保证了数据的一致性和正确性。
parallel的for命令
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
omp_set_num_threads(2);
#pragma omp parallel
{
#pragma omp for
for(int i=0;i<4;i++)
cout << omp_get_thread_num() << endl;
}
}
在给定的示例代码中,#pragma omp for没有带大括号是合法的,因为循环体只有一条语句 :
cout << omp_get_thread_num() << endl;在这种情况下,大括号可以省略。
parallel的for命令
用schedule子句进行for循环任务调度的管理
schedule子句形式
schedule(type, size)
type参数有四种:1.static, 2.dynamic, 3.guided, 4.runtime
size参数是整形数据:表示循环迭代次数划分的单位。
静态调度
不用size参数时分配给每个程序的都是n/t次连续迭代,n为迭代次数,t为并行的线程数目。
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
omp_set_num_threads(2);
#pragma omp parallel for schedule(static)
for(int i=0;i<8;i++)
cout << omp_get_thread_num() << endl;
}
dynamic参数
动态调度模式是先到先得的方式进行任务分配,不用size参数的时候,先把任务干完的线程先取下一个任务,以此类推,而不是一开始就分配固定的任务数。使用size参数的时候,分配的任务以size为单位,一次性分配size个。虽然很智能,在任务难度不均衡的时候适合用dynamic,否则会引起过多的任务动态申请的开销。
guided参数
刚开始每个线程会分配到比较大的迭代块,后来分配到的迭代块逐渐递减,没有指定size就会降到1,否则降到size。
sections制导指令
用sections把不同的区域交给不同的线程去执行.
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
omp_set_num_threads(3);
#pragma omp parallel sections
{
#pragma omp section
{
cout <<omp_get_thread_num();
}
#pragma omp section
{
cout << omp_get_thread_num();
}
#pragma omp section
{
cout << omp_get_thread_num();
}
}
}
这样,每个线程将独立执行一个部分,而不会相互干扰。每个部分的执行顺序是不确定的,可以由系统动态决定。
single制导指令
single制导指令所包含的代码段只有一个线程执行,别的线程跳过该代码,如果没有nowait子句,那么其他线程将会在single制导指令结束的隐式同步点等待。有nowait子句其他线程将跳过等待往下执行(#pragma omp single nowait)。
#pragma omp single指令用于标记只能由一个线程执行的代码块。这个代码块只会被其中一个线程执行,而其他线程会跳过这个代码块。这在需要确保只有一个线程执行某个任务时非常有用。
#include<omp.h>
#include<iostream>
using namespace std;
int main()
{
omp_set_num_threads(4);
#pragma omp parallel
{
#pragma omp single
{
cout << "single thread=" << omp_get_thread_num()<<endl;
}
cout << omp_get_thread_num() << endl;
}
}
解决多线程竞争
#include <stdio.h>
#include <omp.h>
#include <stdlib.h>
int main(void) {
int sum = 0;
#pragma omp parallel for reduction(+:sum)
for (int i=1; i<=100; i++) {
sum += i;
}
printf("%d", sum);
return 0;
}
在OpenMP中,reduction是用于对并行循环中的变量进行归约操作的指令。它能够自动将每个线程的局部变量的结果累加到一个全局变量中,从而避免了竞争条件和数据冲突。
在示例代码中,#pragma omp parallel for reduction(+:sum)指令将变量sum声明为一个归约变量,并指定了归约操作为加法+。
在并行循环中,每个线程都会拥有自己的sum的局部副本,并在循环的迭代过程中进行累加操作。当循环结束后,reduction指令将会将每个线程的sum的局部副本的结果相加,并将最终的总和存储到全局的sum变量中。这样就保证了并行计算的正确性。
在示例代码中,循环中的每个线程将自己的局部sum累加到全局sum中,最后打印出最终的总和。
请注意,reduction指令支持的操作除了加法+以外,还包括减法-、位异或^、位与&、位或|、最大值max和最小值min。你可以根据需要选择适当的归约操作来实现其他类型的归约计算。
总之,reduction指令是OpenMP中用于对并行循环中的变量进行归约操作的指令,能够自动将每个线程的局部变量的结果累加到一个全局变量中。在你的示例代码中,reduction(+:sum)将循环中的每个线程的局部sum累加到全局sum中。
也可以声明多个:
#include <stdio.h>
#include <omp.h>
int main() {
int sum = 0;
int count = 0;
#pragma omp parallel for reduction(+:sum) reduction(+:count)
for (int i = 0; i <= 100; i++) {
sum += i;
count++;
}
printf("Sum: %d\n", sum);
printf("Count: %d\n", count);
double average = static_cast<double>(sum) / count;
printf("Average: %f\n", average);
return 0;
}
临界区
判断最大值
#include <iostream>
#include <omp.h>
int main() {
int max = -1;
omp_set_num_threads(20);
#pragma omp parallel
{
int local_max = -1;
#pragma omp for
for (int i = 1; i <= 100; i++) {
if (i > local_max) {
local_max = i;
}
}
#pragma omp critical
{
if (local_max > max) {
max = local_max;
}
}
}
std::cout << "Max: " << max << std::endl;
return 0;
}
#pragma omp critical是OpenMP中的一个指令,用于创建一个临界区(critical section)。
临界区是一段代码,在同一时间只能由一个线程执行。临界区中的代码是为了保护共享资源,避免竞态条件(race condition)的发生。
当一个线程进入临界区时,其他线程必须等待,直到当前线程退出临界区。这样可以确保在同一时间只有一个线程能够访问临界区中的代码,从而避免并发访问共享资源引发的问题。
在归约操作中,#pragma omp critical用于确保只有一个线程进行更新操作,以避免多个线程同时修改共享变量引发的竞态条件。
在之前的示例代码中,我们将归约操作放在了#pragma omp critical指令内部,这样每个线程在计算局部最大值后,都会尝试进入临界区,只有一个线程能够成功进入临界区并更新最大值max。这样可以保证线程安全并得到正确的最大值结果。
总结而言,#pragma omp critical用于创建临界区,确保在同一时间只有一个线程执行临界区中的代码,用于保护共享资源的并发访问。
矩阵所有元素+1
#include <iostream>
#include <opencv2/opencv.hpp>
#include <omp.h>
int main() {
cv::Mat mat(100, 100, CV_32S);
// 初始化矩阵
for (int i = 0; i < mat.rows; i++) {
for (int j = 0; j < mat.cols; j++) {
mat.at<int>(i, j) = 0;
}
}
// 并行化操作
#pragma omp parallel for
for (int i = 0; i < mat.rows; i++) {
for (int j = 0; j < mat.cols; j++) {
mat.at<int>(i, j) += 1;
}
}
// 输出结果
for (int i = 0; i < mat.rows; i++) {
for (int j = 0; j < mat.cols; j++) {
std::cout << mat.at<int>(i, j) << " ";
}
std::cout << std::endl;
}
return 0;
}
任务池
在这个示例中,我们使用OpenMP的#pragma omp task指令创建了一个任务池。我们使用#pragma omp parallel创建一个并行区域,并使用#pragma omp single指定只有一个线程负责添加任务。
在循环中,我们使用#pragma omp task将任务task_func(i)添加到任务池中。task_func是一个简单的函数,它接受任务ID作为参数,打印任务ID和执行任务的线程号。
通过添加任务到任务池,每个线程可以根据可用的线程资源从任务池中获取任务并执行。OpenMP运行时系统会自动负责任务的调度和执行。
#include <iostream>
#include <omp.h>
void task_func(int task_id) {
std::cout << "Task " << task_id << " is executed by Thread " << omp_get_thread_num() << std::endl;
}
int main() {
const int num_tasks = 10;
// 在代码中设置任务池的总线程数
omp_set_num_threads(num_threads);
// 设置任务池
#pragma omp parallel
{
// 使用single制导指令限制任务添加阶段只由一个线程执行
#pragma omp single
{
for (int i = 0; i < num_tasks; i++) {
// 添加任务到任务池,不加此句均被同一线程执行
#pragma omp task
task_func(i);
}
}
// 等待所有任务执行完毕
#pragma omp taskwait
}
return 0;
}
同步点
使用 #pragma omp parallel 创建一个并行区域,其中指定了总线程数为4。在并行区域中,每个线程首先打印出自己的线程ID,然后遇到了 #pragma omp barrier。
#pragma omp barrier 会等待所有线程都到达这个同步点,然后才会继续执行后续的代码。在这个例子中,当所有线程都到达这个同步点后,每个线程会打印出 “Thread <thread_id> executing after barrier”,其中 <thread_id> 是线程的ID。
#include <iostream>
#include <omp.h>
int main() {
#pragma omp parallel num_threads(4)
{
int thread_id = omp_get_thread_num();
std::cout << "Thread " << thread_id << " executing before barrier" << std::endl;
#pragma omp barrier
std::cout << "Thread " << thread_id << " executing after barrier" << std::endl;
}
return 0;
}
shared和private
shared子句将变量标记为共享变量,意味着并行区域中的所有线程都可以访问和修改该变量的值。private子句将变量标记为私有变量,表示每个线程都有自己的私有拷贝,线程之间的修改不会互相影响。
下面将默认shared变量声明为private,输出结果为0:
#include <iostream>
#include <omp.h>
int main() {
int shared_var = 0;
#pragma omp parallel for private(shared_var)
for (int i = 0; i < 10; i++) {
#pragma omp atomic
shared_var =shared_var+ i+2;
std::cout << "Thread " << omp_get_thread_num() << ": shared_var = " << shared_var << std::endl;
}
std::cout << "Final shared_var = " << shared_var << std::endl;
return 0;
}
单语句原子操作#pragma omp atomic
#include <iostream>
#include <omp.h>
int main() {
int shared_var = 0;
#pragma omp parallel for
for (int i = 0; i < 10; i++) {
#pragma omp atomic
shared_var =shared_var+ i+2;
std::cout << "Thread " << omp_get_thread_num() << ": shared_var = " << shared_var << std::endl;
}
std::cout << "Final shared_var = " << shared_var << std::endl;
return 0;
}
复杂样例程序
#include <stdio.h>
#include <omp.h>
#define THRESHOLD 9
int fib(int n)
{
int i, j;
if (n<2)
return n;
//具体来说,firstprivate 修饰符将变量标记为“首次私有”。在并行执行过程中,它会为每个线程创建一个私有副本,并将变量的初始值作为私有副本的初始值。这样,每个线程都有自己的一份该变量的副本,且初始值与主线程相同。
#pragma omp task shared(i) firstprivate(n) final(n <= THRESHOLD)
i=fib(n-1);
#pragma omp task shared(j) firstprivate(n) final(n <= THRESHOLD)
j=fib(n-2);
#pragma omp taskwait
return i+j;
}
int main()
{
int n = 30;
omp_set_dynamic(0);
omp_set_num_threads(4);
#pragma omp parallel shared(n)
{
#pragma omp single
printf ("fib(%d) = %d\n", n, fib(n));
}
}
具体解释如下:
shared(i): 指定变量 i 为共享变量,这意味着任务可以访问和修改这个变量。
firstprivate(n): 指定变量 n 为私有变量,并将其初始值传递给任务的副本。这样每个任务都会获得一个私有的 n 副本,而不会互相影响。
final(n <= THRESHOLD): 定义任务的终止条件。如果 n 的值小于等于 THRESHOLD,则当前任务将不再创建新的子任务,作为递归的终止条件。这样可以避免过多的任务创建和调度开销。
在斐波那契数列计算中,这行指令用于创建和调度计算 fib(n-1) 的子任务。i 是一个共享变量,可以在任务中对其进行访问和修改。n 是一个私有变量,每个任务都有自己的副本,并使用传入的初始值。
通过 final(n <= THRESHOLD),当 n 的值小于等于 THRESHOLD 时,不再创建新的任务,直接进行计算。这样可以在递归深度较浅时,避免过多的任务创建和调度开销,提高效率