@threads dan @spawn

Menjalankan loop paralel dengan @threads dan penjadwalan dinamis dengan @spawn.

Daftar isi

Paralelisme thread membagi pekerjaan ke beberapa thread sistem operasi pada satu proses, dan Julia menyediakannya tanpa pustaka tambahan melalui makro Threads.@threads untuk pembagian statis serta Threads.@spawn untuk penjadwalan dinamis. Topik ini membahas cara memulai Julia dengan lebih dari satu thread, pola loop paralel yang hasilnya pasti, pengambilan hasil dari pekerjaan yang dilempar secara dinamis, serta sifat balapan data (data race) yang perlu dihindari pada penulisan memori bersama. Setelah topik ini dipraktikkan, loop berat dapat dibagi ke seluruh inti prosesor tanpa mengorbankan ketepatan hasil.

Memulai Julia dengan beberapa thread#

Jumlah thread Julia ditetapkan saat proses dimulai melalui opsi baris perintah -t atau --threads, dan nilai bawaannya adalah satu thread sehingga seluruh pekerjaan berjalan serial. Pengaturan ini sengaja dibuat eksplisit agar perbandingan antara eksekusi serial dan paralel dapat dilakukan dengan adil pada perintah yang sama.

bash
# memulai Julia dengan dua thread pekerja
julia -t 2
text Transkrip terminal
julia> Threads.nthreads()
2

Nilai Threads.nthreads() menyatakan banyaknya thread yang tersedia, termasuk thread utama. Seluruh contoh topik ini dijalankan dengan perintah julia -t 2, sedangkan contoh yang tidak bergantung pada jumlah thread tetap benar bila dijalankan pada pengaturan lain. Variabel lingkungan JULIA_NUM_THREADS juga dapat dipakai untuk menetapkan jumlah thread tanpa menyebut opsi pada setiap perintah, dan pilihannya bergantung pada kebiasaan proyek yang bersangkutan.

Loop paralel dengan @threads#

Threads.@threads for membagi rentang perulangan ke thread-thread yang tersedia, dan setiap iterasi berjalan sekali dengan indeks yang pasti. Pola yang menjaga ketepatan hasil adalah menulis hasil setiap iterasi ke elemen array yang berbeda, sehingga thread-thread bekerja pada memori yang tidak saling bersinggungan.

julia kuadrat-threads.jl
# kuadrat-threads.jl
# Menghitung kuadrat dengan loop paralel, satu hasil per indeks.

n = 8
hasil = zeros(n)
Threads.@threads for i in 1:n
    hasil[i] = i * i
end
println(hasil)
bash
# menjalankan berkas dengan dua thread
julia -t 2 kuadrat-threads.jl
text Keluaran
[1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0]

Keluaran pada contoh sama persis dengan perhitungan serial karena pembagian pekerjaan tidak mengubah isi tiap iterasi, melainkan hanya urutan waktunya. Pola menulis per indeks inilah yang membedakan loop paralel dari akumulasi pada satu nilai bersama, sebab penambahan pada satu variabel oleh beberapa thread saling menimpa. Pembagian rentangnya dilakukan statis, yaitu setiap thread menerima potongan indeks yang sama panjangnya, sehingga beban terbagi merata bila biaya tiap iterasi seragam.

Penjadwalan dinamis dengan @spawn#

Threads.@spawn ekspresi menempatkan ekspresi sebagai tugas pada kumpulan thread tanpa menunggu, dan mengembalikan Task yang hasilnya diambil dengan fetch. Berbeda dari @threads yang membagi rentang secara statis, @spawn menjadwalkan setiap tugas pada thread yang sedang bebas, sehingga pembagian beban mengikuti kecepatan masing-masing pekerjaan, bukan potongan tetap.

julia Contoh: dua tugas yang dilempar secara dinamis
t1 = Threads.@spawn 20 + 22
t2 = Threads.@spawn 10 * 10
println(fetch(t1), " ", fetch(t2))
text Keluaran
42 100

Objek yang dikembalikan @spawn adalah Task yang sama seperti pada topik sebelumnya, sehingga wait, fetch, dan istaskdone berlaku tanpa perbedaan. Perbedaannya terletak pada penjadwalnya, yaitu kumpulan thread yang berukuran sesuai nilai -t, bukan penjadwal kooperatif tunggal. Pola fetch pada contoh menunggu kedua tugas selesai, dan nilai yang tercetak pasti karena fetch mengembalikan nilai kembalian setiap ekspresi, apa pun urutan penyelesaiannya.

Balapan data dan cara menghindarinya#

Balapan data terjadi ketika dua thread menulis memori yang sama tanpa pengaturan, dan hasilnya bergantung pada urutan eksekusi yang tidak terjamin. Contohnya adalah penambahan pada satu elemen array oleh semua thread, yang keluarannya berbeda antar eksekusi. Blok berikut sengaja tidak dieksekusi karena hasilnya bukan angka yang pasti, dan penjelasan cara menghindarinya ada setelah blok tersebut.

julia
# tidak-dijalankan
# balapan data: hasil penjumlahan ini berubah-ubah antar eksekusi
jumlah = 0
Threads.@threads for i in 1:1000
    jumlah += i
end

Tiga cara menghindarinya dapat dipilih sesuai kebutuhan. Pertama, tulis hasil per indeks ke array yang berbeda, sebagaimana berkas kuadrat-threads.jl, lalu rangkum secara serial setelah loop selesai. Kedua, beri setiap thread satu akumulator sendiri dengan membuat array sepanjang jumlah thread dan mengindeksnya dengan Threads.threadid(), lalu jumlahkan akumulatornya di akhir. Ketiga, lindungi bagian yang saling bersinggungan dengan ReentrantLock, yaitu kunci yang hanya mengizinkan satu thread masuk pada satu waktu, dengan pola lock(kunci) dan unlock(kunci) di sekeliling penulisan bersama. Ketiganya menghasilkan kepastian hasil dengan biaya yang berbeda, dan pilihan yang paling murah adalah pola pertama.

Kesalahan umum#

Kesalahan pertama adalah membandingkan hasil serial dan paralel pada jumlah thread yang berbeda tanpa menyebutnya, karena perbedaan kecepatan yang terlihat kemudian dikaitkan dengan kode, bukan dengan pengaturan -t. Perbandingan yang adil menjalankan kedua versi dengan perintah yang sama, dan perbedaannya hanya pada opsi thread.

Kesalahan kedua adalah mengira @threads menjamin urutan eksekusi antar iterasi. Pembagian statis hanya menjamin setiap indeks dikerjakan tepat sekali, bukan urutan waktunya, sehingga efek samping yang bergantung pada urutan tidak boleh diletakkan di dalamnya. Kesalahan ketiga adalah melempar terlalu banyak tugas kecil melalui @spawn, misalnya satu tugas per elemen untuk jutaan elemen, sehingga biaya penjadwalannya menyamai pekerjaannya sendiri. Gabungkan pekerjaan ke dalam potongan yang layak sebelum dilempar, dan biarkan @threads yang mengurus pembagian ketika polanya memang loop tunggal.

Ringkasan#

  • Jumlah thread ditetapkan saat proses dimulai dengan -t, dan Threads.nthreads() menampilkannya. Nilai bawaan adalah satu thread.
  • Threads.@threads for membagi rentang secara statis, dan ketepatan hasilnya dijaga dengan menulis per indeks ke array yang berbeda.
  • Threads.@spawn menjadwalkan tugas secara dinamis pada kumpulan thread, dan hasilnya diambil dengan fetch. Balapan data dihindari dengan pola per indeks, akumulator per thread, atau kunci.

Referensi#