OpenMP多线程基础使用

2024-08-27 06:44
文章标签 基础 使用 多线程 openmp

本文主要是介绍OpenMP多线程基础使用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OpenMP 是一套 C++ 并行编程框架, 也支持 Forthan,是一个跨平台的多线程实现, 能够使串行代码经过最小的改动自动转化成并行的。具有广泛的适应性。这个最小的改动,有时候只是一行编译原语!具体实现是通过分析编译原语#pragma,将用原语定义的代码块,自动转化成并行的线程去执行。每个线程都将分配一个独立的id. 最后再合并线程结果。

  • 可以并行的条件:
    • 可拆分,代码和变量的前后不能相互依赖。
    • 独立运行,运行时,拥有一定的独有的资源。像独有的线程id等。

0 环境要求

0.1 Windows / Visual Studio 平台

VS 版本不低于2015,都支持 OpenMP . 需要在 IDE 进行设置,才能打开 OpenMP 支持。

  • 设置方式:调试 --> C/C++ --> 语言 --> OpenMP支持

这实际上使用了编译选项/openmp。

0.1 Linux / GCC 平台

Ubuntu 自带的 GCC 5.0.4, 直接支持选项 -fopenmp。后面都使用 ubuntu 系统来实验, windows 在配置好环境之后也可以使用。

1 验证支持 OpenMP

main.cpp

#include <iostream>
using namespace std;int main() {#if _OPENMPcout << " support openmp " << endl;#elsecout << " not support openmp" << endl;#endifreturn 0;
}

编译并运行:

# 编译
g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main main.o# 运行
./main# 输出
support openmp

也可以通过编写 Makefile 来更加优雅地编译代码。Makefile 文件的编写比较简单,这里给出一个示例:
Makefile

CC = g++ -std=c++11 nullstring := 
dep_I_incpath = -I./include/hiredis -I./include
dep_L_libpath = -L./lib
dep_l_libname = -lhiredis
#deps_extra = $(dep_I_incpath) $(dep_L_libpath)  $(dep_l_libname)deps_extra = $(nullstring)#c_flag = -g -Wno-format -fpermissive 
c_flag = -g -pthread -Wno-format -fpermissive -fopenmpc_files =  \
$(wildcard *.cpp) $(wildcard *.c) $(wildcard *.cc)
h_files = \
$(wildcard *.hpp) $(wildcard *.h)
o_files_mess = \
$(patsubst %.cpp,%.o,${c_files}) $(patsubst %.c,%.o,${c_files}) $(patsubst %.cc,%.o,${c_files})
o_files = \
$(filter %.o, $(o_files_mess))
elf_file=  \
mainrm_files = \
*.o *.dep *.elf  *.s *.exeall:$(elf_file)$(elf_file):$(o_files)$(CC) $(c_flag) -o $@ $(o_files) $(deps_extra) 
main.o: main.cpp$(CC) $(c_flag) -o $@ -c $< $(deps_extra) 
#	@echo "c_file is:$(c_files)"; echo "h_files:$(h_files)"; echo "o_files:$(o_files)"clean:rm -rf $(rm_files)run:$(elf_file)./$(elf_file)

2 并行输出

  • 顺序打印 0-10 可以使用 for 循环简单实现:
for(int i=0; i< 10; i++) {cout << i << endl; 
}
  • 用 OpenMP 实现并行输出,只需要在 for 循环的起始位置添加一行编译原语来标记即可:
#pragma omp parallel for num_threads(4)
for(int i=0; i<10; i++) {cout << i << endl; 
}

main.cpp

#include <iostream>
#include <omp.h>using namespace std; int serial_cout() {for(int i=0; i<10; i++) {cout << i << endl; }return 0; 
}int parallel_cout() {#pragma omp parallel for num_threads(4)for(int i=0; i<10; i++) {cout << i << endl; }return 0; 
}int main() {cout << "serial cout: " << endl;serial_cout();cout <<"parallel cout: " << endl;parallel_cout();return 0;
}

编译运行

g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main main.o
./mainserial cout:
0
1
2
3
4
5
6
7
8
9
parallel cout:
0
1
2
36
74
5
8
9

可以看到串行输出的是从 0 到 9 顺序输出,并行输出是乱序的,并且并行输出的每次顺序都不一样,说明 OpenMP 并行起了作用。

2.1 说明

上面并行的代码新增了两行:

  • #include <omp.h>,OpenMP 的编译头文件,包括一些常用API,像获取当前的线程id.

  • #pragma omp parallel for num_threads(4),用编译原语,指定其下面的代码块将会被渲染成多线程的代码,然后再编译。这里使用的线程数为 4

3 三种 private

多个线程同时运行的情况下,变量的管理默认有下面两种情况:

  • 创建线程之前就存在的变量——所有线程共享
  • 线程运行时创建的局部变量——每个线程独有
3.1 private

考虑这样的例子:

#include <stdio.h>int main()
{int i, j;#pragma omp parallel forfor (i = 0; i < 3; i++)for (j = 0; j < 3; j++)printf("i=%d j=%d\n", i, j);
}

编译运行之后,会发现输出结果并不符合预期:

i=1 j=0
i=1 j=1
i=1 j=2
i=0 j=0
i=2 j=0

这是多个线程同时访问和修改相同变量导致的问题,

在 openmp 中,可以使用 private() 命令来那解决,private(value),能让 每个线程都有该变量的独立拷贝,相当于一个同名的局部变量。

#include <stdio.h>int main() {int i, j;#pragma omp parallel for private(i, j)for (i = 0; i < 3; i++)for (j = 0; j < 3; j++)printf("i=%d j=%d\n", i, j);
}
3.2 firstprivate

虽然private声明变量后,每个线程都会生成一个相应的拷贝。但这些线程并不会对他们进行初始化。考虑下面的情况:

#include <stdio.h>int main() {int x = -1;#pragma omp parallel for private(x)for (int i = 0; i < 5; i++)printf("x=%d\n", x);
}

可以看到拷贝出的变量值是随机的:

x=14908664
x=13903992
x=13909720
x=13905304
x=8

想要有初始化的话就需要用到 firstprivate()

#include <stdio.h>int main() {int x = -1;#pragma omp parallel for firstprivate(x)for (int i = 0; i < 5; i++)printf("x=%d\n", x);
}

这样每个变量就都能初始化了

x=-1
x=-1
x=-1
x=-1
x=-1
3.3 lastprivate

考虑这样的例子:

#include <stdio.h>int main() {int x = -1;printf("start x=%d\n", x);#pragma omp parallel for private(x)for (int i = 0; i < 5; i++)printf("x=%d\n", x = i);printf("final x=%d", x);
}

虽然现在我们有了初始化,但循环结束后变量的值还是无法保留:

start x=-1
x=1
x=2
x=3
x=0
x=4
final x=-1

前面使用private时,循环结束后x的值是不会保留的。

lastprivate() 的功能就是在 private 的基础上,能够在循环结束时保留变量的值。

#include <stdio.h>int main() {int x = -1;printf("start x=%d\n", x);#pragma omp parallel for lastprivate(x)for (int i = 0; i < 5; i++)printf("x=%d\n", x = i);printf("final x=%d", x);
}

输出结果:

start x=-1
x=3
x=2
x=1
x=4
x=0
final x=4

注意到,最后一个输出的是3,保存的结果却是4?这是因为 lastprivate 保存的变量是逻辑上的最后的值。从代码运行逻辑上来讲 x 最后的值是 4,所以结果就是 4。

3.4 组合使用

从功能上可以看出,firstprivate 是对功能 private 的扩展,二者是相互替代的关系,所以 不能同时使用(会编译失败);lastprivate 和 private 同样也是相互替代的关系,依旧 不能同时使用(会编译失败);firstprivate 和 lastprivate 在功能上有相互补充关系,可以同时使用。

4 reduction

求1~10000000 之和, 用 16 个线程并行:

#include <iostream>
#include <omp.h>
#include <ctime>using namespace std; int parallel_sum() {#if 0
// TODO : this is incorrect way to reduce !!!long long int sum = 0;#pragma omp parallel for num_threads(16)for(int i=0; i<10000000; i++) {sum +=  i; }cout << sum << endl; 
#endif #if 1 long long int sum = 0;#pragma omp parallel for num_threads(16) reduction(+:sum)for(int i=0; i<10000000; i++) {sum +=  i; }cout << sum << endl; 
#endif return 0; 
}int serial_sum() {long long int sum = 0;for(int i=0; i<10000000; i++) {sum +=  i; }cout << sum << endl; return 0;
}int main() {cout << "serial sum: " << endl;clock_t begin_time1 = clock();    serial_sum();float seconds1 = float(clock( ) - begin_time1) / CLOCKS_PER_SEC;cout << "time: " << seconds1 << "s" << endl;cout <<"parallel sum: " << endl;clock_t begin_time2 = clock();parallel_sum();float seconds2 = float(clock( ) - begin_time2) / CLOCKS_PER_SEC;cout << "time: " << seconds2 << "s" << endl;return 0;
}

编译运行

g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main.o -c main.cpp
g++ -std=c++11  -g -pthread -Wno-format -fpermissive -fopenmp -o main main.o
./mainserial sum:
49999995000000
time: 0.046843s
parallel sum:
49999995000000
time: 0.004178s

不难看出,这里使用 opemmp 并行的时候,计算耗时更少。

4.1 说明

在使用 openmp 并行求和的函数里面,在使用条件编译关掉的那个代码块里面,sum += i 这一行多个线程同时访问 sum 时产生会冲突导致。在这种情况下没法使用 private 对其进行求和,需要使用 reduction() 来解决,具体使用如 openmp 并行求和的函数里面,在使用条件编译打开的那个代码块所示。

reduction 的命令格式是 reduction(operation : variable),其中 operation 是操作类型,variable 则是操作变量。

reduction的作用就是给每个线程创建一个独立的变量,在结束后根据操作类型进行归约。

  • 默认操作包括:
    • 算数运算:+, *, -, max, min
    • 逻辑运算:&&, ||
    • 位运算:&, |, ^

这里我们需要对 sum 进行累加,所以应该使用 reduction(+ : sum)

5 获取线程 id

#define DEFINE_idx auto idx = omp_get_thread_num();
#define _ROWS (omp_get_num_threads())
#pragma omp parallel for num_threads(3) 
for(int i=0; i<10; i++) {DEFINE_idx;printf("- idx is %d, i is %d, total thread num is %d\n", idx, i, _ROWS); 
}// 输出idx is 0, i is 0, total thread num is 3
- idx is 0, i is 1, total thread num is 3
- idx is 0, i is 2, total thread num is 3
- idx is 0, i is 3, total thread num is 3
- idx is 2, i is 7, total thread num is 3
- idx is 2, i is 8, total thread num is 3
- idx is 2, i is 9, total thread num is 3
- idx is 1, i is 4, total thread num is 3
- idx is 1, i is 5, total thread num is 3
- idx is 1, i is 6, total thread num is 3

6 对于首个for循环迭代器的限制

OpenMP 在优化多重 for 语句时,会自动把第一个循环的迭代器(也就是i)设为 private。

比如在前面 private 里面的示例双重for循环中,如果将

#pragma omp parallel for private(i, j)

修改为

#pragma omp parallel for private(j) // 不显式声明i为private

修改后并不会影响循环的正常运行。

这篇关于OpenMP多线程基础使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1110925

相关文章

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

pdfmake生成pdf的使用

实际项目中有时会有根据填写的表单数据或者其他格式的数据,将数据自动填充到pdf文件中根据固定模板生成pdf文件的需求 文章目录 利用pdfmake生成pdf文件1.下载安装pdfmake第三方包2.封装生成pdf文件的共用配置3.生成pdf文件的文件模板内容4.调用方法生成pdf 利用pdfmake生成pdf文件 1.下载安装pdfmake第三方包 npm i pdfma

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

git使用的说明总结

Git使用说明 下载安装(下载地址) macOS: Git - Downloading macOS Windows: Git - Downloading Windows Linux/Unix: Git (git-scm.com) 创建新仓库 本地创建新仓库:创建新文件夹,进入文件夹目录,执行指令 git init ,用以创建新的git 克隆仓库 执行指令用以创建一个本地仓库的

【北交大信息所AI-Max2】使用方法

BJTU信息所集群AI_MAX2使用方法 使用的前提是预约到相应的算力卡,拥有登录权限的账号密码,一般为导师组共用一个。 有浏览器、ssh工具就可以。 1.新建集群Terminal 浏览器登陆10.126.62.75 (如果是1集群把75改成66) 交互式开发 执行器选Terminal 密码随便设一个(需记住) 工作空间:私有数据、全部文件 加速器选GeForce_RTX_2080_Ti

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念