Trong OpenMP, việc quản lý biến trong các vùng song song là rất quan trọng để đảm bảo tính đúng đắn và hiệu suất của chương trình. Các clauses như private, shared, firstprivate, lastprivate, threadprivate, copyin, và copyprivate cung cấp các cơ chế khác nhau để kiểm soát phạm vi và sự khởi tạo của các biến trong ngữ cảnh đa luồng.
private
Clause private khai báo một biến là riêng tư cho mỗi luồng trong một vùng song song. Khi một biến được chỉ định là private, mỗi luồng sẽ tạo ra một bản sao riêng của biến đó. Điều này cho phép mỗi luồng thao tác độc lập trên bản sao của mình mà không ảnh hưởng đến các luồng khác.
firstprivate
firstprivate cũng làm cho biến trở nên riêng tư cho mỗi luồng, nhưng nó còn bổ sung thêm việc khởi tạo. Khi một luồng bước vào vùng song song, bản sao riêng của biến sẽ được khởi tạo bằng giá trị của biến đó tại thời điểm trước khi vùng song song được thực thi. Điều này hữu ích khi bạn muốn sử dụng giá trị ban đầu của một biến trong môi trường song song mà không lo ngại các thay đổi trong vùng song song ảnh hưởng đến biến gốc.
lastprivate
lastprivate được sử dụng để đảm bảo rằng giá trị của biến từ vòng lặp cuối cùng sẽ được lưu giữ. Thông thường, biến lặp của vòng lặp trong một vùng song song có bản sao riêng cho mỗi luồng. Tuy nhiên, nếu bạn cần giá trị cuối cùng của biến lặp sau khi vòng lặp kết thúc, lastprivate sẽ truyền giá trị này từ luồng thực hiện vòng lặp cuối cùng trở về luồng chính.
Lưu ý: Clause lastprivate chỉ định rằng giá trị cuối cùng của biến riêng tư sau khi vùng song song kết thúc sẽ được xác định bởi luồng thực hiện vòng lặp cuối cùng và được giữ lại bên ngoài vùng song song. Đối với mảng, lastprivate sẽ sao chép giá trị của tất cả các phần tử từ lần lặp cuối cùng vào vị trí tương ứng của mảng gốc.
shared
shared chỉ định rằng một biến sẽ được chia sẻ giữa tất cả các luồng trong vùng song song. Giá trị của biến chia sẻ là có thể nhìn thấy bởi tất cả các luồng, và mọi luồng đều có thể đọc và ghi vào nó. Mặc dù các biến thường có hành vi chia sẻ mặc định trong OpenMP, việc sử dụng shared giúp làm rõ ý định của lập trình viên.
threadprivate
threadprivate cho phép tạo ra các bản sao riêng của biến cho mỗi luồng, có thể tồn tại trong suốt vòng đời của chương trình. Khác với private chỉ có hiệu lực trong một vùng song song cụ thể, threadprivate khai báo biến là riêng tư cho từng luồng trên phạm vi toàn cục. Điều này rất hữu ích khi bạn cần mỗi luồng có một bản sao toàn cục riêng của một biến để tránh các vấn đề truy cập đồng thời.
Lưu ý: Nếu bạn cần một biến riêng tư chỉ trong một vùng song song, hãy sử dụng private. Nếu bạn cần mỗi luồng có bản sao toàn cục riêng của biến trong suốt chương trình, hãy sử dụng threadprivate. Biến threadprivate có thể được khởi tạo.
#include <iostream>
#include <omp.h>
#define NUM_THREADS 10
using namespace std;
int global_var_k; // Biến toàn cục
#pragma omp threadprivate(global_var_k) // Khai báo là threadprivate
int main(int argc, char* argv[]){
int local_x = 0, local_y = 2, arr_s[5], local_v = 0;
//omp_set_num_threads(num_threads); // Thiết lập số luồng động
#pragma omp parallel private(local_x) firstprivate(local_y) shared(local_v) num_threads(NUM_THREADS)
{
int thread_id = omp_get_thread_num();
local_x = thread_id; // private cho mỗi luồng
local_y += thread_id; // firstprivate, cộng thêm id
local_v += thread_id; // shared, cộng dồn vào biến chung
global_var_k = thread_id + 2; // threadprivate, mỗi luồng có bản sao riêng
cout << "Thread " << thread_id << ": x = " << local_x << "; y = " << local_y << "; k = " << global_var_k << endl;
}
// Sau vùng parallel: local_x là không xác định, local_y giữ giá trị ban đầu + thread_id của luồng cuối cùng thực hiện, local_v là tổng.
cout << "Main thread: x = " << local_x << "; y = " << local_y << "; v = " << local_v << endl;
// Vùng parallel thứ hai để thể hiện global_var_k
#pragma omp parallel num_threads(6)
{
global_var_k = omp_get_thread_num(); // Mỗi luồng gán giá trị riêng cho global_var_k của nó
cout << "Thread k value -> " << global_var_k << endl;
}
// Vùng parallel for với lastprivate
int last_iter_val = 0; // Biến để lưu giá trị cuối cùng
#pragma omp parallel for lastprivate(last_iter_val)
for (int i = 0; i < 5; ++i) {
last_iter_val = i + 10; // Gán giá trị, luồng thực hiện i=4 sẽ ghi đè lên last_iter_val
arr_s[i] = i; // Phần tử mảng không bị ảnh hưởng bởi lastprivate ở đây
cout << "Loop iter " << i << " in thread " << omp_get_thread_num() << ": last_iter_val = " << last_iter_val << endl;
}
// Sau vòng lặp for: last_iter_val sẽ mang giá trị từ lần lặp cuối cùng (i=4)
cout << "Value after loop (last_iter_val): " << last_iter_val << endl;
cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
cout << arr_s[i] << " "; // In ra các phần tử mảng
}
cout << endl;
return 0;
}
copyin
Clause copyin cung cấp một cơ chế để sao chép giá trị của một biến threadprivate từ luồng chính sang các bản sao threadprivate của các luồng khác khi bắt đầu một vùng song song. Điều này đảm bảo rằng tất cả các luồng bắt đầu với cùng một giá trị ban đầu cho biến threadprivate đó.
#include <iostream>
#include <omp.h>
using namespace std;
int shared_thread_var = 666; // Khai báo là threadprivate
#pragma omp threadprivate(shared_thread_var)
int main(int argc, char* argv[]){
// Vùng parallel đầu tiên, sử dụng copyin
#pragma omp parallel copyin(shared_thread_var)
{
int id = omp_get_thread_num();
// Luồng master thay đổi giá trị
#pragma omp master
{
shared_thread_var = 999;
cout << "[1st Parallel Block - Master] Thread " << id << ": shared_thread_var = " << shared_thread_var << endl;
}
#pragma omp barrier // Đảm bảo luồng master hoàn thành việc thay đổi trước khi các luồng khác đọc
cout << "[1st Parallel Block] Thread " << id << ": shared_thread_var = " << shared_thread_var << endl;
}
// Vùng parallel thứ hai, sử dụng copyin
// shared_thread_var sẽ được khởi tạo lại bằng giá trị của nó TRƯỚC khi vùng parallel này bắt đầu,
// nhờ vào copyin, nó sẽ lấy giá trị đã được thay đổi bởi master ở khối trước.
#pragma omp parallel copyin(shared_thread_var)
{
int id = omp_get_thread_num();
cout << "[2nd Parallel Block] Thread " << id << ": shared_thread_var = " << shared_thread_var << endl;
}
return 0;
}
Lưu ý: Biến được chỉ định trong copyin phải được khai báo là threadprivate. Các biến threadprivate yêu cầu bộ nhớ lưu trữ tĩnh (biến toàn cục hoặc biến cục bộ tĩnh), được cấp phát khi chương trình khởi động và giải phóng khi kết thúc. Biến threadprivate có thể không được khởi tạo và sẽ bị thay đổi.
copyprivate
copyprivate là một clause có thể sử dụng trong các cấu trúc như single. Nó cung cấp một cơ chế để truyền giá trị của một biến riêng tư trong một tác vụ ngầm định (implicit task) tới các tác vụ ngầm định khác trong cùng một vùng song song. Cụ thể, luồng thực hiện khối single sẽ cập nhật giá trị của biến, và sau đó giá trị này sẽ được sao chép đến các luồng khác.
#include <iostream>
#include <omp.h>
using namespace std;
int main(int argc, char* argv[]){
int data_var = 111; // Biến ban đầu
#pragma omp parallel firstprivate(data_var) // data_var là firstprivate cho mỗi luồng
{
int id = omp_get_thread_num();
cout << "[Before Barrier] Thread " << id << ": data_var = " << data_var << endl;
#pragma omp barrier // Chờ tất cả các luồng đạt đến đây
// Khối 'single' sẽ chỉ được thực thi bởi một luồng duy nhất (luồng 0 mặc định)
// copyprivate(data_var) sẽ sao chép giá trị của data_var từ luồng thực thi 'single'
// đến tất cả các luồng khác sau khi khối 'single' hoàn thành.
#pragma omp single copyprivate(data_var)
{
// Luồng này thay đổi data_var
data_var = 333;
cout << "[Inside Single Block] Thread " << id << ": data_var = " << data_var << endl;
}
// Tất cả các luồng sẽ thấy giá trị data_var đã được cập nhật nhờ copyprivate
cout << "[After Barrier/Single] Thread " << id << ": data_var = " << data_var << endl;
}
return 0;
}