Saat sebuah blok kode dijalankan oleh banyak thread, kita harus menentukan variabel mana yang bisa diakses bersama dan mana yang harus dimiliki secara pribadi oleh setiap thread. Ini disebut scope.
shared: Satu variabel asli yang dapat dilihat dan dimodifikasi oleh semua thread. Ini adalah default untuk variabel yang dideklarasikan di luar blok paralel. Potensi race condition sangat tinggi di sini.
private: Setiap thread mendapatkan salinan (kopi) variabelnya sendiri yang terpisah. Perubahan yang dibuat oleh satu thread tidak akan terlihat oleh thread lain. Ini adalah default untuk variabel yang dideklarasikan di dalam blok paralel (termasuk variabel iterator loop).
Masalah Klasik: Race Condition pada Agregasi
Bayangkan setiap thread menghitung hasil lokal (my_result) dan mencoba menambahkannya ke hasil global (global_result).
global_result += my_result; // BAHAYA!
Operasi ini tidak atomic (instan). Ia terdiri dari tiga langkah: (1) Baca global_result, (2) Tambahkan my_result, (3) Tulis kembali ke global_result. Jika dua thread melakukannya bersamaan, salah satu pembaruan bisa hilang.
Solusi Buruk: Menggunakan #pragma omp critical untuk melindungi baris ini. Cara ini aman, tetapi sangat lambat karena memaksa semua thread untuk antre (serialisasi), menghilangkan keuntungan dari paralelisasi.
Solusi Elegan: Klausa reduction
reduction adalah cara OpenMP yang paling efisien dan tepat untuk melakukan operasi agregasi secara paralel dan aman.
Cara Kerja:
Setiap thread secara otomatis mendapatkan salinan private dari variabel reduksi (misal, approx), yang diinisialisasi sesuai operatornya (0 untuk +, 1 untuk *).
Setiap thread bekerja hanya pada salinan private-nya, tanpa ada race condition.
Setelah semua thread selesai, OpenMP akan mengambil semua nilai dari salinan private dan menggabungkannya menjadi satu nilai pada variabel global asli menggunakan operator yang ditentukan.
Sintaks: reduction(<operator>:<variabel>)
#pragma omp parallel for reduction(+: approx)for (i = 1; i <= n - 1; i++) { approx += f(a + i * h); // Aman, 'approx' di sini adalah private}// Di akhir, semua 'approx' private dijumlahkan ke 'approx' global
Operator Umum: +, *, -, & (bitwise AND), | (bitwise OR), ^ (bitwise XOR), && (logical AND), || (logical OR). Versi lebih baru juga mendukung min dan max.
Praktik Terbaik: default(none)
Untuk menghindari kesalahan scope yang tidak disengaja, sangat disarankan untuk menggunakan klausa default(none). Klausa ini memaksa programmer untuk secara eksplisit menentukan scope dari setiap variabel yang digunakan di dalam blok paralel.
Compiler akan memberikan error jika ada variabel yang scope-nya belum ditentukan. Ini adalah jaring pengaman yang sangat baik.
Masalah Dependensi Data
reduction hanya bekerja untuk operasi yang asosiatif dan komutatif. Ia tidak bisa menyelesaikan masalah dependensi data antar iterasi. Contohnya adalah pada perhitungan deret Fibonacci: fibo[i] = fibo[i-1] + fibo[i-2]. Nilai fibo[i] bergantung langsung pada hasil iterasi sebelumnya. Memparalelkan loop semacam ini akan menghasilkan jawaban yang salah, bahkan dengan reduction. Tanggung jawab untuk memastikan iterasi independen ada pada programmer.
Summary
Manajemen data yang aman dalam OpenMP bergantung pada pengaturan scope variabel yang benar (shared atau private). Untuk operasi agregasi, klausa reduction adalah mekanisme yang aman dan efisien untuk menghindari race condition dengan memberikan setiap thread salinan privat dari variabel dan menggabungkan hasilnya di akhir. Menggunakan default(none) adalah praktik krusial untuk memaksa deklarasi scope secara eksplisit dan mencegah bug.
Additional Information
Studi Kasus: Perhitungan π dengan Dependensi Terselubung
Perhatikan loop untuk menghitung π berikut:
double sum = 0.0;double factor = 1.0;// Loop ini memiliki dependensi data pada 'factor'for (k = 0; k < n; k++) { sum += factor / (2*k + 1); factor = -factor; // Nilai 'factor' di iterasi k+1 bergantung pada iterasi k}
Jika kita memparalelkan loop ini, factor yang di-share akan menjadi kacau. Solusinya bukan reduction pada factor, melainkan menghilangkan dependensinya:
double sum = 0.0;#pragma omp parallel for reduction(+:sum) private(factor)for (k = 0; k < n; k++) { if (k % 2 == 0) factor = 1.0; else factor = -1.0; sum += factor / (2*k + 1); // 'factor' kini private dan dihitung ulang}
Dengan membuat factor menjadi private dan menghitung nilainya berdasarkan k (variabel iterator loop yang juga implisitnya private), kita menghilangkan dependensi antar iterasi dan loop bisa diparalelkan dengan aman.
Eksplorasi Mandiri
Coba tulis sebuah program OpenMP untuk mencari nilai maksimum dalam sebuah array besar. Gunakan reduction dengan operator max. (Petunjuk: Anda mungkin perlu versi OpenMP yang lebih baru atau menggunakan trik dengan critical section untuk membandingkan.)
Apa yang akan terjadi jika Anda lupa menambahkan reduction(+: sum) pada contoh perhitungan π di atas, tetapi variabel sum tetap shared? Simulasikan skenario race condition-nya.