async/await mengatasi pekerjaan yang banyak menunggu, sedangkan perulangan paralel mengatasi pekerjaan hitung yang berat dan dapat dipecah. Topik ini membahas Parallel.For dan Parallel.ForEach dari Task Parallel Library beserta syarat agar paralelisme benar-benar menguntungkan. Setelah membacanya, pemilihan antara menunggu dengan await dan membagi kerja ke banyak thread dapat ditentukan dari sifat pekerjaannya.
Dari perulangan biasa ke perulangan paralel#
Parallel.For dan Parallel.ForEach membagi iterasi sebuah perulangan ke beberapa thread pekerja. Parallel.For membagi rentang indeks, sedangkan Parallel.ForEach membagi elemen sebuah koleksi. Tidak ada penulisan khusus yang dituntut pada badan perulangannya, sehingga lambda yang biasa dipakai pada for tetap dipakai di sini.
Perbedaan terpenting terletak pada urutan. Perulangan biasa menjamin iterasi ke-0 selesai sebelum iterasi ke-1 dimulai, sedangkan perulangan paralel menjalankan iterasi dalam urutan yang tidak dapat dijamin dan bahkan bersamaan. Konsekuensinya, program berikut hanya mencetak agregat, yaitu jumlah akhir yang tidak bergantung urutan eksekusi. Salin programnya ke berkas Program.cs, lalu jalankan dengan perintah dotnet run --file Program.cs.
// Program Parallel.For dan Parallel.ForEach dengan agregat deterministik.
int[] data = [4, 8, 15, 16, 23, 42];
long total = 0;
object kunci = new();
Parallel.For(0, data.Length, i =>
{
long kuadrat = (long)data[i] * data[i];
lock (kunci)
{
total += kuadrat;
}
});
Console.WriteLine($"Jumlah kuadrat dengan Parallel.For: {total}");
long totalForEach = 0;
Parallel.ForEach(data, nilai =>
{
long kuadrat = (long)nilai * nilai;
lock (kunci)
{
totalForEach += kuadrat;
}
});
Console.WriteLine($"Jumlah kuadrat dengan Parallel.ForEach: {totalForEach}");Jumlah kuadrat dengan Parallel.For: 2854
Jumlah kuadrat dengan Parallel.ForEach: 2854Nilai 2854 selalu tercetak karena penjumlahan tidak bergantung urutan, meskipun urutan iterasinya berbeda pada setiap eksekusi. Penambahan ke variabel total dilindungi lock karena beberapa thread memperbarui satu variabel yang sama, dan urutan penambahan itu pun tidak terjamin. Apabila kuadrat tiap indeks dicetak dari dalam badan perulangan, urutannya berubah antareksekusi, dan sifat itulah yang harus diakomodasi oleh program paralel, bukan dilawan.
Menulis hasil tanpa tarik-menarik#
Dua strategi membuat perulangan paralel tetap aman tanpa banyak lock. Strategi pertama adalah memberi setiap iterasi wilayah tulisannya sendiri, misalnya menulis ke indeks array yang berbeda. Strategi kedua adalah mengumpulkan hasil tiap iterasi lalu mengurutkan atau merangkumnya setelah perulangan selesai. Contoh berikut memakai keduanya sekaligus.
// Contoh: tiap iterasi menulis ke indeks berbeda lalu hasil diurutkan.
int[] angka = [4, 8, 15, 16, 23, 42];
string[] catatan = new string[angka.Length];
Parallel.For(0, angka.Length, i =>
{
catatan[i] = $"indeks {i} berisi angka {angka[i]}";
});
Array.Sort(catatan);
foreach (string baris in catatan)
{
Console.WriteLine(baris);
}indeks 0 berisi angka 4
indeks 1 berisi angka 8
indeks 2 berisi angka 15
indeks 3 berisi angka 16
indeks 4 berisi angka 23
indeks 5 berisi angka 42Penulisan catatan[i] aman karena setiap iterasi memegang indeks yang tidak dipakai iterasi lain, sehingga tidak ada dua thread yang menulis ke sel yang sama. Pengurutan Array.Sort dilakukan setelah Parallel.For tuntas, sehingga keluarannya terurut deterministik meskipun urutan pengisiannya tidak teratur. Untuk akumulasi angka, lock pada program sebelumnya dapat digantikan oleh Interlocked yang dibahas pada topik Keamanan Thread, sedangkan koleksi yang aman terhadap akses serentak tersedia sebagai ConcurrentDictionary.
Kapan paralelisme unggul#
Perulangan paralel menguntungkan apabila tiga syarat terpenuhi sekaligus. Pekerjaannya bersifat CPU-bound, yaitu menghabiskan waktu prosesor dan bukan menunggu I/O. Iterasinya saling lepas, sehingga hasil satu iterasi tidak dibutuhkan oleh iterasi lain. Jumlah iterasinya cukup besar, karena membagi kerja dan menyalakan thread membutuhkan biaya awal yang menutupi perulangan yang kecil.
Kasus yang cocok antara lain mengolah gambar per blok piksel, menghitung statistik per berkas, dan memvalidasi data dalam jumlah besar. Kasus yang tidak cocok adalah pekerjaan I/O seperti permintaan jaringan, karena await menahan nol thread selama menunggu, sedangkan paralelisme menahan satu thread per pekerjaan, sehingga async jauh lebih hemat. Badan iterasi yang hanya berisi beberapa operasi aritmetika juga tidak cocok karena biaya koordinasinya melebihi manfaatnya.
Derajat paralelisme dapat dikendalikan melalui ParallelOptions. Properti MaxDegreeOfParallelism membatasi jumlah thread yang dipakai, dan pembatasan ini berguna pada mesin bersama agar satu proses tidak menghabiskan seluruh inti prosesor.
Nilai kembali kedua metode tersebut berupa ParallelLoopResult. Properti IsCompleted pada hasil itu menyatakan seluruh iterasi dijalankan sampai akhir, sedangkan nilai LowestBreakIteration terisi apabila perulangan dihentikan lebih awal melalui ParallelLoopState.Break. Fasilitas ini dipakai apabila pekerjaan boleh berhenti lebih awal, misalnya pencarian yang cukup berhenti pada temuan pertama. Sebelum memutuskan paralel, kebutuhan waktu juga diukur terlebih dahulu, karena perbaikan yang diduga besar sering ternyata kecil setelah diukur.
Kesalahan umum#
Empat kesalahan berikut paling sering muncul ketika perulangan paralel dipakai.
- Mengandalkan urutan iterasi. Urutan eksekusi paralel tidak terjamin, sehingga logika yang bergantung urutan dipindahkan ke luar perulangan, misalnya dengan mengurutkan hasil setelah perulangan selesai.
- Memakai Parallel.For untuk pekerjaan I/O. Setiap iterasi menahan satu thread saat menunggu, sehingga sumber daya terbuang dan manfaat paralelisme tidak muncul. Pekerjaan semacam ini lebih tepat di-await bersama dengan WhenAll.
- Memodifikasi koleksi yang sedang diiterasi. Parallel.ForEach melempar
InvalidOperationExceptionapabila koleksinya berubah di tengah perulangan, sehingga perubahan dikumpulkan terlebih dahulu dan diterapkan setelahnya. - Memakai perulangan paralel untuk badan yang terlalu kecil. Biaya membagi kerja melebihi manfaatnya, sehingga perulangan biasa lebih cepat untuk ratusan operasi aritmetika sederhana.
Ringkasan#
- Parallel.For membagi rentang indeks dan Parallel.ForEach membagi elemen koleksi ke beberapa thread, dengan urutan eksekusi yang tidak terjamin.
- Program yang aman memberi tiap iterasi wilayah tulis terpisah atau melindungi keadaan bersama dengan lock, lalu merangkum hasil setelah perulangan selesai.
- Paralelisme unggul untuk pekerjaan CPU-bound yang iterasinya saling lepas dan cukup banyak, sedangkan pekerjaan I/O lebih tepat di-await bersama dengan WhenAll.
Referensi#
- Data Parallelism (Task Parallel Library) — Microsoft Learn — Panduan Parallel.For dan Parallel.ForEach beserta variasi overload-nya.
- Potential Pitfalls in Data and Task Parallelism — Microsoft Learn — Daftar jebakan paralelisme data beserta cara menghindarinya.