OpenMP學習 第十章 超越通用核心的多線程
第十章 超越通用核心的多線程
基于通用核心的附加子句
- 并行構造的附加子句:
- num_threads(integer-expression)
用于設置線程總數. - if(scalar-expression)
用于為并行構造提供條件分支. - copyin(list)
- proc_bind(master|close|spread)
- num_threads(integer-expression)
為了測試num_threads子句與if子句的用法,構造下面所示原型:
#include <iostream>
#include <omp.h>
int main()
{
int NTHREAD, x;
std::cin >> x;
#pragma omp parallel if(x>1) \
num_threads(x/2) //通過\來實現跨行
{
if(omp_get_thread_num()==0)
NTHREAD = omp_get_num_threads();
}
std::cout << "the num of thread is: " << NTHREAD;
return 0;
}
通過程序實驗,證明了其相關用法.
-
共享工作循環構造的附加子句:
- lastprivate(list)
如同private與firstprivate子句一樣,其為列表中每個變量創建一個私有副本,在區域結束時,列表中每個變量的原始變量將被賦值為最后一次迭代值. - schedule子句中的附加調度類型:
- 啟發式調度(guided):動態調度的另一種形式,其中chunk_size一開始是一個大值,每次執行新的分塊迭代后chunk_size都會減少,直至chunk_size的最小值.
- 自動調度(auto):編譯器和運行時根據自己的選擇來安排循環迭代.
- 運行時調度(runtime):調度和可能的chunk_size來自內部控制變量.
據文檔所言,其提供了omp_set_schedule與omp_get_schedule兩個函數與omp_sched_t枚舉類來處理.但是,實際編碼時并未成功使用相關函數及枚舉類.
- collapse(n)
規定共享工作循環構造之后的n個循環將被合并為一個隱式循環.任何額外的子句,包括數據環境子句或歸約都會應用到這個隱式循環中.
- lastprivate(list)
-
任務構造的附加值局:
- untied
該子句用于限制任務隊列增長,在任務隊列增長時避免任務隊列增長速度過大. - priority(priority-value)
該子句可以顯式提示任務的執行優先級,優先級值的范圍為[0,max-task-prioriity-var].
最大值可以通過環境變量OMP_MAX_TASK_PRIORITY設置.
也可以通過omp_get_max_task_priority(void)函數查詢. - depend(dependence-type:list)
用于處理依賴情況下的任務,分析模式類似于DAG(有向無環圖).
其中dependence-type包括out,in和inout三種,帶有in依賴類型的變量會導致任務等待另一個任務完成,該任務在帶有out依賴類型的子句中具有相同的變量. - if(scalar-expression)
如果if子句中表達式為false,那么任務將不會被延遲執行. - final(scalar expression)
當final子句中的表達式為true,那么任務將會被立即執行. - mergeable
用于指示編譯器是否可以將兩個或多個連續的任務合并為一個任務.
- untied
-
創建一個顯式任務調度點:
#pragma omp taskyield
- 創建一個任務循環構造:
#pragma omp taskloop [clause[, clause] ...]
//for-loop
- 創建一個同步任務組:
#pragma omp taskgroup [clause[, clause] ...]
{
//body of taskgroup
}
為了理解depend子句的使用,下面通過一個實例來幫助理解:
#include <omp.h>
int main()
{
int A,B,C,G,F;
#pragma omp parallel shared(A,B,C,G,F)
{
#pragma omp task depend(out:A)
TaskA(&A);
#pragma omp task depend(in:A,G)
TaskB(&B);
#pragma omp task depend(in:A) depend(out:C)
TaskC(&C);
#pragma omp task depend(in:A) depend(out:G)
TaskG(&G);
#pragma omp task depend(in:C,G)
TaskF(&F);
}
return 0;
}
通用核心中缺失的多線程功能
- threadprivate
OpenMP的基本內存模型將內存視為一組給內存中的地址命名的變量.除了shared和private兩類,OpenMP還定義了第三種內存類型: threadprivate.
threadprivate內存是一個線程的私有內存,它不能被其他線程訪問.然而,其內存中的變量在各個例程中具有可見性. 在非正式情況下,可以認為threadprivate內存是線程的私有內存.它不能被其他線程訪問.
threadprivate是一個聲明性指令,這意味著它出現在程序中聲明變量的地方,并影響其聲明的語義.
- 聲明threadprivate內存:
#pragma omp threadprivate(list)
為了理解threadprivate的使用,我們回到第七章所述的鏈表程序:
#include <iostream>
#include <cstdlib>
#include <omp.h>
import <format>;
#define NODE_NUM 20
#define CHUNK 2
#define NTHREADS 3
typedef struct node {
int data;
int procResult;
struct node* next;
node() :data(0), procResult(0), next(nullptr) {}
}Node, * List;
int count = 0;
#pragma omp threadprivate(count)
void incCount()
{
count++;
return;
}
void initList(List p)
{
Node* root{ p };
Node* temp_node;
p->data = 0;
for (int i = 1; i < NODE_NUM; i++) {
temp_node = new Node;
temp_node->data = i;
root->next = temp_node;
root = temp_node;
}
return;
}
void processWork(Node* n)
{
n->procResult = (n->data * n->data);
return;
}
void deleteList(List p)
{
Node* temp_node = p->next;
for (; p != temp_node;) {
temp_node = p;
while (temp_node->next != nullptr && temp_node->next->next != nullptr)
temp_node = temp_node->next;
delete temp_node->next;
temp_node->next = nullptr;
}
delete p;
return;
}
int main()
{
List list = new Node;
Node** parr = new Node * [NODE_NUM];
initList(list);
#ifdef NTHREADS
omp_set_num_threads(NTHREADS);
#endif // NTHREADS
Node* p;
#pragma omp parallel
{
#pragma omp single
{
p = list;
while (p != nullptr)
{
#pragma omp task firstprivate(p)
{
incCount();
processWork(p);
std::cout << std::format("in the {} thrd, the count is {}",
omp_get_thread_num(),
count
) << std::endl;
}//end of task creation
p = p->next;
}
}//end of single region
}//end of parallel region
deleteList(list);
return 0;
}
我們在鏈表程序的基礎上添加了一個threadprivate內存的count,用于統計在線程中執行的task數量.threadprivate數據與特定線程相綁定,因此會在程序中引入錯誤源.
- master
master 構造定義了一個由線程組的主線程執行的工作塊.與single構造不同,它的構造末尾沒有隱式的柵欄.
- 聲明一個master構造:
#pragma omp master
{
//body of master
}
- atomic
atomic 構造確保了一個變量作為一個獨立的,不間斷的動作被讀取,寫入或更新.其保護了一個變量,避免了并發線程對一個存儲位置進行多次同步更新的可能性.
atomic構造與critical構造有很大共同點,如果多個線程試圖同時執行一個atomic構造,"第一個線程"將執行原子操作,而其他線程將等待輪到自己
atomic構造中通過子句定義原子操作的類型,其中最常見的有三種:讀,寫和更新(不包括捕獲).默認情況(不包含子句)是更新.
| clause | 原子操作示例 |
|---|---|
| read | v=x; |
| write | x=expr; |
| update (default) |
x++;x--;++x;--x; x = expr;v = expr(x); |
現在讓我們回到第四章中關于Pi數值積分的部分.
#include <iostream>
#include <omp.h>
#include <fstream>
import <format>;
#define TURNS 100
#define PI 3.141592653589793
long double num_steps = 1e8;
double step;
int main()
{
std::ofstream out;
out.open("example.csv", std::ios::ate);
out << "NTHREADS,pi,err,run_time,num_steps" << std::endl;
double sum = 0.0;
for (int NTHREADS = 1; NTHREADS < TURNS; NTHREADS++) {
double start_time, run_time;
double pi, err;
pi = sum = 0.0;
int actual_nthreads;
step = 1.0 / (double)num_steps;
omp_set_num_threads(NTHREADS);
start_time = omp_get_wtime();
#pragma omp parallel
{
int id = omp_get_thread_num();
int numthreads = omp_get_num_threads();
double x;
double partial = 0.0;
if (id == 0)
actual_nthreads = numthreads;
int istart = id * num_steps / numthreads;
int iend = (id + 1) * num_steps / numthreads;
if (id == (numthreads - 1))
iend = num_steps;
for (int i = istart; i < iend; i++) {
x = (i + 0.5) * step;
partial += 4.0 / (1.0 + x * x);
}
#pragma omp atomic
sum += partial;
}//end of parallel
pi = step * sum;
err = pi - PI;
run_time = omp_get_wtime() - start_time;
std::cout << std::format("pi is {} in {} seconds {} thrds.step is {},err is {}",
pi,
run_time,
actual_nthreads,
step,
err
) << std::endl;
out << std::format("{},{:.15f},{:.15f},{:.15f},{}",
NTHREADS,
pi,
err,
run_time,
num_steps
) << std::endl;
}
out.close();
return 0;
}
我們在這里將critical構造更改為atomic構造實現了相同的功能.
然而,雖然類似于critical構造,但是atomic構造只適用于直接涉及內存中存儲位置的操作,也就是說:
#pragma omp atomic
full_sum+=foo();
其中函數foo()的執行不受atomic構造的保護,其等價于:
tmp = foo();
#pragma omp atomic
full_sum+=tmp;
這意味著foo()執行過程中很可能發生數據競爭.
- OMP_STACKSIZE
OpenMP被設計為支持多種系統,操作系統代表正在執行的程序對進程進行管理.
進程分叉出與其關聯的線程.
當操作系統創建線程時,它為每個線程預留了一些本地內存,這個內存以棧的形式進行管理.
棧的大小是有限的,如果在線程內部運行的代碼創建了大的對象,棧內存可能會溢出,導致潛在的災難性失敗.
為了解決這個問題,OpenMP定義了一個叫做stacksize-var的內部控制變量,它控制線程組中每個線程相關聯的內存棧的大小.
設置stacksize-var的命令如下:
export OMP_STACKSIZE=size
OpenMP定義了一系列單位用于處理size:
- size設置以1024字節為單位的大小
- sizeB設置以1字節為單位的大小
- sizeK設置以1024字節為單位的大小
- sizeM設置以1024 * 1024字節為單位的大小
- sizeG設置以1024 * 1024 * 1024字節為單位的大小
舉例:
export OMP_STACKSIZE="200K"http://200*1024 bytes
- omp_get_max_threads
omp_get_num_threads用于詢問OpenMP運行時線程組有多少個線程,但是只能在同一個并行區域內調用.
但是有時候,需要一個可以從并行區域外調用的函數,以找到后續parallel構造所創建的線程組中可能獲得的最大線程數..
此時就應當使用 omp_get_max_threads.
int omp_get_max_threads(void)
為了理解其使用,我們提供下面一個例子:
#include <iostream>
#include <omp.h>
int main()
{
int nthread_1, nthread_2;
omp_set_num_threads(2);
nthread_1 = omp_get_max_threads();
#pragma omp parallel
{
if (omp_get_thread_num() == 0)
std::cout << nthread_1 << std::endl;
}
omp_set_num_threads(4);
nthread_2 = omp_get_max_threads();
#pragma omp parallel
{
if (omp_get_thread_num() == 0)
std::cout << nthread_2 << std::endl;
}
return 0;
}
其最終得到的結果為:
2
4
證明了我們調用omp_get_max_threads()所得到的結果的正確.
- omp_set_dynamic
一個OpenMP程序通常由多個被并行區域分隔的順序部分組成.OpenMP運行時會嘗試對一個并行區域到下一個并行區域時,優化線程組的大小,這成為動態模式(dynamic mode).
這意味著OpenMP運行時必須假定與線程相關聯的資源可能在并行區域之間發生變化.如果希望在并行區域之間重用線程資源,則需要告訴運行時系統關閉動態線程調度的功能.
通過omp_set_dynamic(),我們可以啟用或禁用動態模式.
- 啟用或禁用動態模式:
void omp_set_dynamic(int dyn_threads)
其中dyn_threads為一個bool值,其為true時將允許線程組大小再并行區域之間變化.
- omp_in_parallel
讓活動線程的數量超過物理核心的數量會影響性能,因為操作系統會因為郭隊線程交換而消耗資源,這就是所謂的認購超額.
因此,有些時候想知道自己是否在一個活躍的并行區域內,這樣就可以調整后續并行區域中創建的線程數量.
omp_in_parallel()函數用于查詢代碼是否在并行區域內,如果在活動的并行區域內,那么返回true.
- 查詢代碼是否在并行區域內:
void omp_in_parallel();

浙公網安備 33010602011771號