網(wǎng)站首頁 編程語言 正文
前言
在前面的教程OpenMP入門當(dāng)中我們簡(jiǎn)要介紹了 OpenMP 的一些基礎(chǔ)的使用方法,在本篇文章當(dāng)中我們將從一些基礎(chǔ)的問題開始,然后仔細(xì)介紹在 OpenMP 當(dāng)中 reduction 子句的各種使用方法。
從并發(fā)求和開始
我們的任務(wù)是兩個(gè)線程同時(shí)對(duì)一個(gè)變量 data
進(jìn)行 ++
操作,執(zhí)行 10000 次,我們看下面的代碼有什么問題:
#include <stdio.h> #include <omp.h> #include <unistd.h> static int data; int main() { #pragma omp parallel num_threads(2) // 使用兩個(gè)線程同時(shí)執(zhí)行上面的代碼塊 { for(int i = 0; i < 10000; i++) { data++; usleep(10); } // omp_get_thread_num 函數(shù)返回線程的 id 號(hào) 這個(gè)數(shù)據(jù)從 0 開始,0, 1, 2, 3, 4, ... printf("data = %d tid = %d\n", data, omp_get_thread_num()); } printf("In main function data = %d\n", data); return 0; }
在上面的代碼當(dāng)中,我們開啟了兩個(gè)線程并且同時(shí)執(zhí)行 $pragma
下面的代碼塊,但是上面的程序有一個(gè)問題,就是兩個(gè)線程可能同時(shí)執(zhí)行 data++
操作,但是同時(shí)執(zhí)行這個(gè)操作的話,就存在并發(fā)程序的數(shù)據(jù)競(jìng)爭(zhēng)問題,在 OpenMP 當(dāng)中默認(rèn)的數(shù)據(jù)使用方式就是??♂?線程之間是共享的比如下面的執(zhí)行過程:
- 首先線程 1 和線程 2 將 data 加載到 CPU 緩存當(dāng)中,當(dāng)前的兩個(gè)線程得到的
data
的值都是 0 。 - 線程 1 和線程 2 對(duì)
data
進(jìn)行 ++ 操作,現(xiàn)在兩個(gè)線程的data
的值都是 1。 - 線程 1 將 data 的值寫回到主存當(dāng)中,那么主存當(dāng)中的數(shù)據(jù)的值就等于 1 。
- 線程 2 將 data 的值寫回到主存當(dāng)中,那么主存當(dāng)中的數(shù)據(jù)的值也等于 1 。
但是上面的執(zhí)行過程是存在問題的,因?yàn)槲覀兤谕氖侵鞔娈?dāng)中的 data 的值等于 2,因此上面的代碼是存在錯(cuò)誤的。
解決求和問題的各種辦法
使用數(shù)組巧妙解決并發(fā)程序當(dāng)中的數(shù)據(jù)競(jìng)爭(zhēng)問題
在上面的程序當(dāng)中我們使用了一個(gè)函數(shù) omp_get_thread_num
這個(gè)函數(shù)可以返回線程的 id 號(hào),我們可以根據(jù)這個(gè) id 做一些文章,如下面的程序:
#include <stdio.h> #include <omp.h> #include <unistd.h> static int data; static int tarr[2]; int main() { #pragma omp parallel num_threads(2) { int tid = omp_get_thread_num(); for(int i = 0; i < 10000; i++) { tarr[tid]++; usleep(10); } printf("tarr[%d] = %d tid = %d\n", tid, tarr[tid], tid); } data = tarr[0] + tarr[1]; printf("In main function data = %d\n", data); return 0; }
在上面的程序當(dāng)中我們額外的使用了一個(gè)數(shù)組 tarr
用于保存線程的本地的和,然后在最后在主線程里面講線程本地得到的和相加起來,這樣的話我們得到的結(jié)果就是正確的了。
$./lockfree01.out
tarr[1] = 10000 tid = 1
tarr[0] = 10000 tid = 0
In main function data = 20000
在上面的程序當(dāng)中我們需要知道的是,只有當(dāng)并行域當(dāng)中所有的線程都執(zhí)行完成之后,主線程才會(huì)繼續(xù)執(zhí)行并行域后面的代碼,因此主線程在執(zhí)行代碼
data = tarr[0] + tarr[1]; printf("In main function data = %d\n", data);
之前,OpenMP 中并行域中的代碼全部執(zhí)行完成,因此上面的代碼執(zhí)行的時(shí)候數(shù)組 tarr
中的結(jié)果已經(jīng)計(jì)算出來了,因此上面的代碼最終的執(zhí)行結(jié)果是 2000。
reduction 子句
在上文當(dāng)中我們使用數(shù)組去避免多個(gè)線程同時(shí)操作同一個(gè)數(shù)據(jù)的情況,除了上面的方法處理求和問題,我們還有很多其他方法去解決這個(gè)問題,下面我們使用 reduction 子句去解決這個(gè)問題:
#include <stdio.h> #include <omp.h> #include <unistd.h> static int data; int main() { #pragma omp parallel num_threads(2) reduction(+:data) { for(int i = 0; i < 10000; i++) { data++; usleep(10); } printf("data = %d tid = %d\n", data, omp_get_thread_num()); } printf("In main function data = %d\n", data); return 0; }
在上面的程序當(dāng)中我們使用了一個(gè)子句 reduction(+:data)
在每個(gè)線程里面對(duì)變量 data 進(jìn)行拷貝,然后在線程當(dāng)中使用這個(gè)拷貝的變量,這樣的話就不存在數(shù)據(jù)競(jìng)爭(zhēng)了,因?yàn)槊總€(gè)線程使用的 data 是不一樣的,在 reduction 當(dāng)中還有一個(gè)加號(hào)?,這個(gè)加號(hào)表示如何進(jìn)行規(guī)約操作,所謂規(guī)約操作簡(jiǎn)單說來就是多個(gè)數(shù)據(jù)逐步進(jìn)行操作最終得到一個(gè)不能夠在進(jìn)行規(guī)約的數(shù)據(jù)。
例如在上面的程序當(dāng)中我們的規(guī)約操作是 + ,因此需要將線程 1 和線程 2 的數(shù)據(jù)進(jìn)行 + 操作,即線程 1 的 data 加上 線程 2 的 data 值,然后將得到的結(jié)果賦值給全局變量 data,這樣的話我們最終得到的結(jié)果就是正確的。
如果有 4 個(gè)線程的話,那么就有 4 個(gè)線程本地的 data(每個(gè)線程一個(gè) data)。那么規(guī)約(reduction)操作的結(jié)果等于:
(((data1 + data2) + data3) + data4) 其中 datai 表示第 i 個(gè)線程的得到的 data 。
除了后面的兩種方法解決多個(gè)線程同時(shí)對(duì)一個(gè)數(shù)據(jù)進(jìn)行操作的問題的之外我們還有一些其他的辦法去解決這個(gè)問題,我們?cè)谙乱黄恼庐?dāng)中進(jìn)行仔細(xì)分析。
深入剖析 reduction 子句
我們?cè)趯懚嗑€程程序的時(shí)候可能會(huì)存在這種需求,每個(gè)線程都會(huì)得到一個(gè)數(shù)據(jù)的結(jié)果,然后在最后需要將每個(gè)線程得到的數(shù)據(jù)進(jìn)行求和,相乘,或者邏輯操作等等,在這種情況下我們可以使用 reduction 子句進(jìn)行操作。redcution 子句的語法格式如下:
reduction(操作符:變量)
當(dāng)我們使用 reduction 子句的時(shí)候線程使用的是與外部變量同名的變量,那么這個(gè)同名的變量的初始值應(yīng)該設(shè)置成什么呢?具體的設(shè)置規(guī)則如下所示:
運(yùn)算符 | 初始值 |
---|---|
+/加法 | 0 |
*/乘法 | 1 |
&&/邏輯與 | 1 |
||/邏輯或 | 0 |
min/最小值 | 對(duì)應(yīng)類型的最大值 |
max/最大值 | 對(duì)應(yīng)類型的最小值 |
&/按位與 | 所有位都是 1 |
|/按位或 | 所有位都是 0 |
^/按位異或 | 所有位都是 0 |
下面我們使用各種不同的例子去分析上面的所有的條目:
加法+操作符
我們使用下面的程序去測(cè)試使用加法規(guī)約的正確性,并且在并行域當(dāng)中打印進(jìn)行并行域之前變量的值。
#include <stdio.h> #include <omp.h> static int data; int main() { #pragma omp parallel num_threads(2) reduction(+:data) { printf("初始值 : data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 10; }else if(omp_get_thread_num() == 1){ data = 20; } printf("變化后的值 : data = %d tid = %d\n", data, omp_get_thread_num()); } printf("規(guī)約之后的值 : data = %d\n", data); return 0; }
上面的程序的輸出結(jié)果如下所示:
初始值 : data = 0 tid = 0
變化后的值 : data = 10 tid = 0
初始值 : data = 0 tid = 1
變化后的值 : data = 20 tid = 1
規(guī)約之后的值 : data = 30
從上面的輸出結(jié)果我們可以知道當(dāng)進(jìn)入并行域之后我們的變量的初始值等于 0 ,第一個(gè)線程的線程 id 號(hào)等于 0 ,它將 data 的值賦值成 10 ,第二個(gè)線程的線程 id 號(hào) 等于 1,它將 data 的值賦值成 20 。在出并行域之前會(huì)將兩個(gè)線程得到的 data 值進(jìn)行規(guī)約操作,在上面的代碼當(dāng)中也就是+操作,并且將這個(gè)值賦值給全局變量 data 。
乘法*操作符
#include <stdio.h> #include <omp.h> static int data = 2; int main() { #pragma omp parallel num_threads(2) reduction(*:data) { printf("初始值 : data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 10; }else if(omp_get_thread_num() == 1){ data = 20; } printf("變化后的值 : data = %d tid = %d\n", data, omp_get_thread_num()); } printf("規(guī)約之后的值 : data = %d\n", data); return 0; }
上面的程序輸出結(jié)果如下所示:
初始值 : data = 1 tid = 0
變化后的值 : data = 10 tid = 0
初始值 : data = 1 tid = 1
變化后的值 : data = 20 tid = 1
規(guī)約之后的值 : data = 400
從上面的程序的輸出結(jié)果來看,當(dāng)我們使用*操作符的時(shí)候,我們可以看到程序當(dāng)中 data 的初始值確實(shí)被初始化成了 1 ,而且最終在主函數(shù)當(dāng)中的輸出結(jié)果也是符合預(yù)期的,因?yàn)?400 = 2 * 10 * 20,其中 2 只在全局變量初始化的時(shí)候的值。
邏輯與&&操作符
#include <stdio.h> #include <omp.h> static int data = 100; int main() { #pragma omp parallel num_threads(2) reduction(&&:data) { printf("data =\t %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 10; }else if(omp_get_thread_num() == 1){ data = 20; } } printf("data = %d\n", data); return 0; }
上面的程序的輸出結(jié)果如下所示:
初始化值 : data = 1 tid = 0
初始化值 : data = 1 tid = 1
在主函數(shù)當(dāng)中 : data = 1
從上面的輸出結(jié)果我們可以知道,程序當(dāng)中數(shù)據(jù)的初始化的值是沒有問題的,你可能會(huì)疑惑為什么主函數(shù)當(dāng)中的 data 值等于 1,這其實(shí)就是 C 語言當(dāng)中對(duì) && 操作服的定義,如果最終的結(jié)果為真,那么值就等于 1,即 100 && 10 && 20 == 1,你可以寫一個(gè)程序去驗(yàn)證這一點(diǎn)。
或||操作符
#include <stdio.h> #include <omp.h> static int data = 100; int main() { #pragma omp parallel num_threads(2) reduction(||:data) { printf("初始化值 : data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 0; }else if(omp_get_thread_num() == 1){ data = 0; } } printf("在主函數(shù)當(dāng)中 : data = %d\n", data); return 0; }
上面的程序輸出結(jié)果如下所示:
初始化值 : data = 1 tid = 0
初始化值 : data = 1 tid = 1
在主函數(shù)當(dāng)中 : data = 1
從上面的結(jié)果看出,數(shù)據(jù)初始化的值是正確的,主函數(shù)當(dāng)中得到的數(shù)據(jù)也是正確的,因?yàn)?100 || 0 || 0 == 1,這個(gè)也是 C 語言的條件或得到的結(jié)果。
MIN 最小值
#include <stdio.h> #include <omp.h> static int data = 1000; int main() { printf("Int 類型的最大值等于 %d\n", __INT32_MAX__); #pragma omp parallel num_threads(2) reduction(min:data) { printf("data =\t\t %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 10; }else if(omp_get_thread_num() == 1){ data = 20; } } printf("data = %d\n", data); return 0; }
上面的程序執(zhí)行結(jié)果如下所示:
Int 類型的最大值等于 ? 2147483647
data = ? ? ? ? ? ? ? 2147483647 tid = 0
data = ? ? ? ? ? ? ? 2147483647 tid = 1
data = 10
?
可以看出來初始化的值是正確的,當(dāng)我們求最小值的時(shí)候,數(shù)據(jù)被正確的初始化成對(duì)應(yīng)數(shù)據(jù)的最大值了,然后我們需要去比較這幾個(gè)值的最小值,即 min(1000, 0, 20) == 10 ,因此在主函數(shù)當(dāng)中的到的值等于 10。
MAX 最大值
#include <stdio.h> #include <omp.h> static int data = 1000; int main() { #pragma omp parallel num_threads(2) reduction(max:data) { printf("data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 10; }else if(omp_get_thread_num() == 1){ data = 20; } } printf("data = %d\n", data); return 0; }
上面的程序輸出結(jié)果如下所示:
data = -2147483648 tid = 0
data = -2147483648 tid = 1
data = 1000
可以看出程序被正確的初始化成最小值了,主函數(shù)當(dāng)中輸出的數(shù)據(jù)應(yīng)該等于 max(1000, 10, 20) 因此也滿足條件。
& 按位與
#include <stdio.h> #include <omp.h> static int data = 15; int main() { #pragma omp parallel num_threads(2) reduction(&:data) { printf("data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 8; }else if(omp_get_thread_num() == 1){ data = 12; } } printf("data = %d\n", data); return 0; }
上面的程序輸出結(jié)果如下:
data = -1 tid = 0
data = -1 tid = 1
data = 8
首先我們需要知道上面幾個(gè)數(shù)據(jù)的比特位表示:
-1 = 1111_1111_1111_1111_1111_1111_1111_1111 8 = 0000_0000_0000_0000_0000_0000_0000_1000 12 = 0000_0000_0000_0000_0000_0000_0000_1100 15 = 0000_0000_0000_0000_0000_0000_0000_1111
我們知道當(dāng)我們使用 & 操作符的時(shí)候初始值是比特為全部等于 1 的數(shù)據(jù),也就是 -1,最終進(jìn)行按位與操作的數(shù)據(jù)為 15、8、12,即在主函數(shù)當(dāng)中輸出的結(jié)果等于 (8 & 12 & 15) == 8,因?yàn)橹挥械谒膫€(gè)比特位全部為 1,因此最終的結(jié)果等于 8 。
|按位或
#include <stdio.h> #include <omp.h> static int data = 1; int main() { #pragma omp parallel num_threads(2) reduction(|:data) { printf("data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 8; }else if(omp_get_thread_num() == 1){ data = 12; } } printf("data = %d\n", data); return 0; }
上面的程序輸出結(jié)果如下所示:
data = 0 tid = 0
data = 0 tid = 1
data = 13
我們還是需要了解一下上面的數(shù)據(jù)的比特位表示:
0 = 0000_0000_0000_0000_0000_0000_0000_0000 1 = 0000_0000_0000_0000_0000_0000_0000_0001 8 = 0000_0000_0000_0000_0000_0000_0000_1000 12 = 0000_0000_0000_0000_0000_0000_0000_1100 13 = 0000_0000_0000_0000_0000_0000_0000_1101
線程初始化的數(shù)據(jù)等于 0 ,這個(gè)和前面談到的所有的比特位都設(shè)置成 0 是一致的,我們對(duì)上面的數(shù)據(jù)進(jìn)行或操作之后得到的結(jié)果和對(duì)應(yīng)的按位或得到的結(jié)果是相符的。
^按位異或
#include <stdio.h> #include <omp.h> static int data = 1; int main() { #pragma omp parallel num_threads(2) reduction(^:data) { printf("data = %d tid = %d\n", data, omp_get_thread_num()); if(omp_get_thread_num() == 0) { data = 8; }else if(omp_get_thread_num() == 1){ data = 12; } } printf("data = %d\n", data); return 0; }
上面的程序的輸出結(jié)果如下所示:
data = 0 tid = 0
data = 0 tid = 1
data = 5
各個(gè)數(shù)據(jù)的比特位表示:
0 = 0000_0000_0000_0000_0000_0000_0000_0000 1 = 0000_0000_0000_0000_0000_0000_0000_0001 8 = 0000_0000_0000_0000_0000_0000_0000_1000 12 = 0000_0000_0000_0000_0000_0000_0000_1100 5 = 0000_0000_0000_0000_0000_0000_0000_0101
大家可以自己對(duì)照的進(jìn)行異或操作,得到的結(jié)果是正確的。
總結(jié)
在本篇文章當(dāng)中我們主要使用一個(gè)例子介紹了如何解決并發(fā)程序當(dāng)中的競(jìng)爭(zhēng)問題,然后也使用了 reduction 子句去解決這個(gè)問題,隨后介紹了在 OpenMP 當(dāng)中 reduction 各種規(guī)約符號(hào)的使用!
在本篇文章當(dāng)中主要給大家介紹了 OpenMP 的基本使用和程序執(zhí)行的基本原理,在后續(xù)的文章當(dāng)中我們將仔細(xì)介紹各種 OpenMP
的子句和指令的使用方法,希望大家有所收獲!
原文鏈接:https://juejin.cn/post/7163929178335608863
相關(guān)推薦
- 2022-07-11 Python內(nèi)建類型bytes深入理解_python
- 2022-11-27 通過源碼分析Golang?cron的實(shí)現(xiàn)原理_Golang
- 2022-09-01 基于MFC實(shí)現(xiàn)單個(gè)文檔的文件讀寫_C 語言
- 2022-10-05 Python數(shù)據(jù)可視化制作全球地震散點(diǎn)圖_python
- 2022-05-01 Entity?Framework系統(tǒng)架構(gòu)與原理介紹_基礎(chǔ)應(yīng)用
- 2022-04-08 從頭學(xué)習(xí)C語言之二維數(shù)組_C 語言
- 2023-04-03 Python調(diào)試神器之PySnooper的使用教程分享_python
- 2022-03-14 Linux磁盤格式化和掛載(linux服務(wù)器硬盤掛載步驟)
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支