一、OpenMP基本概念
OpenMP是一種用于共享內(nèi)存并行系統(tǒng)的多線程程序設(shè)計方案,支持的編程語言包括C、C++和Fortran。OpenMP提供了對并行算法的高層抽象描述,特別適合在多核CPU機器上的并行程序設(shè)計。編譯器根據(jù)程序中添加的pragma指令,自動將程序并行處理,使用OpenMP降低了并行編程的難度和復(fù)雜度。當編譯器不支持OpenMP時,程序會退化成普通(串行)程序。程序中已有的OpenMP指令不會影響程序的正常編譯運行。在VS中啟用OpenMP很簡單,很多主流的編譯環(huán)境都內(nèi)置了OpenMP。(具體介紹可以參考OpenMP總結(jié))
二、OpenMP執(zhí)行模式
OpenMP采用fork-join的執(zhí)行模式。開始的時候只存在一個主線程,當需要進行并行計算的時候,派生出若干個分支線程來執(zhí)行并行任務(wù)。當并行代碼執(zhí)行完成之后,分支線程會合,并把控制流程交給單獨的主線程。
一個典型的fork-join執(zhí)行模型的示意圖如下:
OpenMP編程模型以線程為基礎(chǔ),通過編譯制導(dǎo)指令制導(dǎo)并行化,有三種編程要素可以實現(xiàn)并行化控制,他們分別是編譯制導(dǎo)、API函數(shù)集和環(huán)境變量。
三、編譯制導(dǎo)
編譯制導(dǎo)指令以#pragma omp 開始,后邊跟具體的功能指令,格式如:#pragma omp 指令[子句[,子句] …]。
3.1 常用的功能指令
功能指令 | 解析 |
---|---|
parallel | 用在一個結(jié)構(gòu)塊之前,表示這段代碼將被多個線程并行執(zhí)行 |
for | 用于for循環(huán)語句之前,表示將循環(huán)計算任務(wù)分配到多個線程中并行執(zhí)行,以實現(xiàn)任務(wù)分擔(dān),必須由編程人員自己保證每次循環(huán)之間無數(shù)據(jù)相關(guān)性 |
sections | 用在可被并行執(zhí)行的代碼段之前,用于實現(xiàn)多個結(jié)構(gòu)塊語句的任務(wù)分擔(dān),可并行執(zhí)行的代碼段各自用section指令標出(注意區(qū)分sections和section) |
parallel sections | parallel和sections兩個語句的結(jié)合,類似于parallel for |
single | 用在并行域內(nèi),表示一段只被單個線程執(zhí)行的代碼 |
critical | 用在一段代碼臨界區(qū)之前,保證每次只有一個OpenMP線程進入 |
flush | 保證各個OpenMP線程的數(shù)據(jù)影像的一致性 |
barrier | 用于并行域內(nèi)代碼的線程同步,線程執(zhí)行到barrier時要停下等待,直到所有線程都執(zhí)行到barrier時才繼續(xù)往下執(zhí)行 |
atomic | 用于指定一個數(shù)據(jù)操作需要原子性地完成 |
master | 用于指定一段代碼由主線程執(zhí)行 |
threadprivate | 用于指定一個或多個變量是線程專用,后面會解釋線程專有和私有的區(qū)別 |
3.2 相應(yīng)的OpenMP子句
OpenMP子句 | 解析 |
---|---|
private | 指定一個或多個變量在每個線程中都有它自己的私有副本 |
firstprivate | 指定一個或多個變量在每個線程都有它自己的私有副本,并且私有變量要在進入并行域或任務(wù)分擔(dān)域時,繼承主線程中的同名變量的值作為初值 |
lastprivate | 是用來指定將線程中的一個或多個私有變量的值在并行處理結(jié)束后復(fù)制到主線程中的同名變量中,負責(zé)拷貝的線程是for或sections任務(wù)分擔(dān)中的最后一個線程 |
reduction | 用來指定一個或多個變量是私有的,并且在并行處理結(jié)束后這些變量要執(zhí)行指定的歸約運算,并將結(jié)果返回給主線程同名變量 |
nowait | 指出并發(fā)線程可以忽略其他制導(dǎo)指令暗含的路障同步 |
num_threads | 指定并行域內(nèi)的線程的數(shù)目 |
schedule | 指定for任務(wù)分擔(dān)中的任務(wù)分配調(diào)度類型 |
shared | 指定一個或多個變量為多個線程間的共享變量 |
ordered | 用來指定for任務(wù)分擔(dān)域內(nèi)指定代碼段需要按照串行循環(huán)次序執(zhí)行 |
copyprivate | 配合single指令,將指定線程的專有變量廣播到并行域內(nèi)其他線程的同名變量中 |
copyinn | 用來指定一個threadprivate類型的變量需要用主線程同名變量進行初始化 |
default | 用來指定并行域內(nèi)的變量的使用方式,缺省是shared |
四、API函數(shù)
除上述編譯制導(dǎo)指令之外,OpenMP還提供了一組API函數(shù)用于控制并發(fā)線程的某些行為,下面是一些常用的OpenMP API函數(shù)以及說明
函數(shù)名 | 作用 |
---|---|
omp_in_parallel | 判斷當前是否在并行域中 |
omp_get_thread_num | 返回線程號 |
omp_set_num_thread | 設(shè)置后續(xù)并行域中的線程格式 |
omp_get_num_threads | 返回當前并行域中的線程數(shù) |
omp_get_max_threads | 返回并行域可用的最大線程數(shù)目 |
omp_get_num_prpces | 返回系統(tǒng)中處理器的數(shù)目 |
omp_get_dynamic | 判斷是否支持動態(tài)改變線程數(shù)目 |
omp_set_dynamic | 啟用或關(guān)閉線程數(shù)目的動態(tài)改變 |
omp_get_nested | 判斷系統(tǒng)是否支持并行嵌套 |
omp_set_nested | 啟用或關(guān)閉并行嵌套 |
五、環(huán)境變量
OpenMP中定義一些環(huán)境變量,可以通過這些環(huán)境變量控制OpenMP程序的行為,常用的環(huán)境變量
環(huán)境變量 | 解析 |
---|---|
OMP_SCHEDULE | 用于for循環(huán)并行化后的調(diào)度,它的值就是循環(huán)調(diào)度的類型 |
OMP_NUM_THREADS | 用于設(shè)置并行域中的線程數(shù) |
OMP_DYNAMIC | 通過設(shè)定變量值,來確定是否允許動態(tài)設(shè)定并行域內(nèi)的線程數(shù) |
OMP_NESTED | 指出是否可以并行嵌套 |
六、簡單示例
6.1 parallel使用
parallel制導(dǎo)指令用來創(chuàng)建并行域,后邊要跟一個大括號將要并行執(zhí)行的代碼放在一起
test_para.cpp
#include<iostream>
#include"omp.h"
using namespace std;
int main()
{
#pragma omp parallel
{
cout << "Test" << endl;
}
return 0;
}
編譯:g++ -fopenmp test_para.cpp -o test_para
運行:./op_test
結(jié)果:打印了16個Test(筆者電腦是16核,所以打印16個)
6.2 paraller for使用
使用parallel制導(dǎo)指令只是產(chǎn)生了并行域,讓多個線程分別執(zhí)行相同的任務(wù),并沒有實際的使用價值。parallel for用于生成一個并行域,并將計算任務(wù)在多個線程之間分配,從而加快計算運行的速度。可以讓系統(tǒng)默認分配線程個數(shù),也可以使用num_threads子句指定線程個數(shù)。
test_parafor.c
#include<stdio.h>
#include <stdlib.h>
#include<omp.h>
int main(int argc,char** argv)
{
#pragma omp parallel for num_threads(6)
for (int i = 0; i < 12; i++)
{
printf("OpenMP Test, 線程編號為: %d\n", omp_get_thread_num());
}
return 0;
}
編譯:gcc -fopenmp test_parafor.c -o test_parafor
運行:./test_parafor
結(jié)果:如下圖
上邊程序指定了6個線程,迭代量為12,從輸出可以看到每個線程都分到了12/6=2次的迭代量。
6.3 OpenMP效率提升以及不同線程數(shù)效率對比
diff_threads.c
#include <stdlib.h>
#include <stdio.h>
#include "omp.h"
void test()
{
for (int i = 0; i < 80000; i++)
{
//do something
}
}
int main(int argc, char **argv)
{
float startTime = omp_get_wtime();
//指定2個線程
#pragma omp parallel for num_threads(2)
for (int i = 0; i < 80000; i++)
{
test();
}
float endTime = omp_get_wtime();
printf("指定 2 個線程,執(zhí)行時間: %f\n", endTime - startTime);
startTime = endTime;
//指定4個線程
#pragma omp parallel for num_threads(4)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 4 個線程,執(zhí)行時間: %f\n", endTime - startTime);
startTime = endTime;
//指定8個線程
#pragma omp parallel for num_threads(8)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 8 個線程,執(zhí)行時間: %f\n", endTime - startTime);
startTime = endTime;
//指定12個線程
#pragma omp parallel for num_threads(12)
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("指定 12 個線程,執(zhí)行時間: %f\n", endTime - startTime);
startTime = endTime;
//不使用OpenMP
for (int i = 0; i < 80000; i++)
{
test();
}
endTime = omp_get_wtime();
printf("不使用OpenMP多線程,執(zhí)行時間: %f\n", endTime - startTime);
startTime = endTime;
return 0;
}
編譯:gcc -fopenmp diff_threads.c -o diff_threads
運行:./diff_threads
結(jié)果:如下圖
可見,使用OpenMP優(yōu)化后的程序執(zhí)行時間是原來的1/4左右,并且并不是線程數(shù)使用越多效率越高,一般線程數(shù)達到4~8個的時候,不能簡單通過提高線程數(shù)來進一步提高效率。
6.4 API使用
API.c
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
printf("ID: %d, Max threads: %d, Num threads: %d \n", omp_get_thread_num(), omp_get_max_threads(), omp_get_num_threads());
omp_set_num_threads(5);
printf("ID: %d, Max threads: %d, Num threads: %d \n", omp_get_thread_num(), omp_get_max_threads(), omp_get_num_threads());
#pragma omp parallel num_threads(5)
{
// omp_set_num_threads(6); // Do not call it in parallel region
printf("ID: %d, Max threads: %d, Num threads: %d \n", omp_get_thread_num(), omp_get_max_threads(), omp_get_num_threads());
}
printf("ID: %d, Max threads: %d, Num threads: %d \n", omp_get_thread_num(), omp_get_max_threads(), omp_get_num_threads());
omp_set_num_threads(6);
printf("ID: %d, Max threads: %d, Num threads: %d \n", omp_get_thread_num(), omp_get_max_threads(), omp_get_num_threads());
return 0;
}
編譯:gcc -fopenmp API.c -o api
運行:./api
結(jié)果:如下圖