Topik ini membahas cara menjalankan sebagian pekerjaan program Nim pada thread lain. Pembahasan mencakup peluncuran tugas dengan spawn dari modul std/threadpool, pembacaan hasil melalui FlowVar, penungguan seluruh tugas dengan sync, serta proc khusus thread yang ditandai pragma {.thread.} beserta penungguannya, joinThread. Semua contoh membagi satu pekerjaan hitung menjadi empat bagian yang setara, sehingga keluaran program dapat diperiksa ulang dengan hitungan manual dan tetap identik pada setiap eksekusi.
Meluncurkan tugas dengan spawn#
spawn menerima sebuah panggilan proc dan mengeksekusinya di thread lain tanpa membuat objek thread secara manual. Dokumentasi std/threadpool menyatakan bahwa spawn selalu meluncurkan tugas baru, sehingga panggilan itu tidak pernah dijalankan pada thread pemanggil. Nilai kembalian spawn adalah FlowVar[T], wadah untuk hasil yang belum tentu sudah terisi pada saat baris spawn selesai dijalankan. Pembacaan isi wadah dilakukan dengan operator ^, yang menunggu sampai nilai tersedia baru mengembalikannya. Kolam thread (thread pool), yaitu kumpulan thread pekerja yang siap menerima tugas, dibuat otomatis oleh modul ini sehingga program tidak perlu mengelola pembuatan thread satu per satu.
Program berikut membagi penjumlahan kuadrat dari 1 sampai 100 menjadi empat bagian. Setiap bagian diluncurkan dengan spawn, lalu hasilnya dibaca berurutan dengan ^.
import std/threadpool
proc jumlahKuadrat(awal: int, akhir: int): int =
var total = 0
for i in awal .. akhir:
total += i * i
total
proc main() =
let batas = [1, 26, 51, 76]
var tugas: array[4, FlowVar[int]]
for i in 0 .. 3:
tugas[i] = spawn jumlahKuadrat(batas[i], batas[i] + 24)
var total = 0
for i in 0 .. 3:
let bagian = ^tugas[i]
echo "bagian ", i, ": ", bagian
total += bagian
echo "total: ", total
main()# kompilasi program spawn
nim c --warning[Deprecated]:off --hints:off jumlah_spawn.nim
# jalankan program hasil kompilasi
./jumlah_spawnbagian 0: 5525
bagian 1: 37400
bagian 2: 100525
bagian 3: 194900
total: 338350Keluaran bernilai tetap pada setiap eksekusi karena urutan pencetakan ditegakkan oleh urutan pembacaan. Empat tugas boleh berjalan dalam urutan apa pun di kolam thread, tetapi baris ^tugas[i] menahan thread utama sampai bagian i selesai, sehingga bagian 0 selalu tercetak lebih dahulu, disusul bagian 1 dan seterusnya. Nilai tiap bagian dapat diperiksa ulang, misalnya jumlah kuadrat 1 sampai 25 memang 5525, dan jumlah keempatnya adalah 338350.
Menunggu tugas selesai dengan sync#
sync adalah penghalang (barrier) yang menunggu seluruh tugas yang pernah diluncurkan lewat spawn sampai benar-benar selesai. Proc ini dipakai ketika hasil tidak dikembalikan sebagai FlowVar, melainkan ditulis ke lokasi memori yang sudah disiapkan. Pola yang umum adalah menyediakan satu slot per thread, yaitu satu elemen array untuk setiap bagian tugas, lalu mengirimkan alamat slotnya lewat tipe ptr yang aman dikirim antar thread. Tidak ada dua thread yang menulis ke slot yang sama, sehingga setelah sync kembali, semua slot sudah terisi dan thread utama dapat membacanya dalam urutan mana pun.
import std/threadpool
type
TugasKuadrat = object
mulai: int
slot: ptr int
proc hitungKuadrat(arg: TugasKuadrat): int =
var total = 0
for i in arg.mulai .. (arg.mulai + 24):
total += i * i
arg.slot[] = total
total
let batas = [1, 26, 51, 76]
var hasil: array[4, int]
for i in 0 .. 3:
discard spawn hitungKuadrat(TugasKuadrat(mulai: batas[i], slot: addr hasil[i]))
sync()
var total = 0
for i in 0 .. 3:
echo "bagian ", i, ": ", hasil[i]
total += hasil[i]
echo "total: ", total# kompilasi program penungguan sync
nim c --warning[Deprecated]:off --hints:off jumlah_sync.nim
# jalankan program hasil kompilasi
./jumlah_syncbagian 0: 5525
bagian 1: 37400
bagian 2: 100525
bagian 3: 194900
total: 338350Keluarannya sama persis dengan contoh spawn sebelumnya karena perhitungannya identik. Perbedaannya ada pada jalur komunikasinya. Hasil tidak lewat FlowVar, melainkan lewat slot hasil[i] yang alamatnya diteruskan sebagai argumen, dan pemanggilan sync menjadi titik yang menjamin seluruh penulisan slot sudah selesai sebelum pembacaan dimulai. Tanpa sync, thread utama bisa membaca slot yang belum ditulis, dan keluarannya menjadi tidak terprediksi.
Diagram berikut merangkum dua jalur itu, yaitu hasil melalui FlowVar dan hasil melalui slot bersama dengan penungguan sync.
Proc khusus thread dan joinThread#
Pragma {.thread.} menandai sebuah proc sebagai proc khusus thread, yaitu proc yang dirancang untuk dijalankan pada thread lain. Penandaan ini menyiratkan {.gcsafe.}, sehingga proc semacam itu tidak boleh menyentuh memori yang dikelola pengelola memori secara langsung, sebagaimana dibahas pada topik Keamanan GC. Modul std/threads memakai pragma ini bersama tipe Thread[T]: createThread meluncurkan satu thread dengan satu argumen bertipe T, sedangkan joinThread menunggu thread tersebut selesai. Pasangan ini memberi kendali penuh atas pembuatan dan penungguan thread, berbeda dengan kolam thread yang anggotanya dikelola otomatis.
# tidak-dijalankan
import std/threads
proc kerja(id: int) {.thread.} =
echo "thread ", id, " berjalan"
var t: Thread[int]
t.createThread(kerja, 7)
joinThread(t)Pola di atas meluncurkan satu thread dengan argumen 7 dan menunggunya selesai sebelum program berlanjut. joinThread bersifat menunggu sampai selesai (blocking), sehingga setelah baris itu terlewati, seluruh efek proc kerja sudah terjadi. Untuk beberapa thread sekaligus, tersedia joinThreads yang menerima sejumlah thread sekaligus. Disiplin yang perlu dijaga adalah memasangkan setiap createThread dengan satu joinThread, karena program yang berakhir meninggalkan thread yang belum selesai tanpa peringatan.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika thread di Nim baru dipraktikkan.
- Menganggap urutan peluncuran
spawnsama dengan urutan eksekusi. Tugas yang diluncurkan lebih dulu dapat selesai lebih belakang, sehingga urutan kerja di kolam thread tidak dapat diandalkan. Tegakkan urutan pada sisi pembacaan, yaitu lewat^yang dipanggil berurutan atau pembacaan slot setelahsync. - Melupakan penungguan. Program yang tidak memanggil
syncataujoinThreaddapat berakhir sebelum tugasnya selesai, sehingga sebagian keluaran tidak pernah muncul. Pastikan setiap tugas mempunyai titik tunggu yang jelas. - Bertukar data lewat variabel global biasa. Beberapa thread yang menulis ke variabel global yang sama saling menimpa tanpa aturan, dan hasilnya berbeda antar eksekusi. Serahkan hasil melalui
FlowVar, slot per thread, atau antrean pesan bertipe seperti channel.
Ringkasan#
spawnmenjalankan panggilan proc di thread lain dan mengembalikanFlowVar[T], sedangkan^menunggu sampai nilai hasilnya tersedia.syncmenunggu seluruh tugas yang pernah diluncurkan, dan pola slot per thread melaluiptrmenjaga setiap thread menulis ke lokasinya sendiri.- Proc khusus thread ditandai
{.thread.}, diluncurkan dengancreateThread, dan ditunggu denganjoinThreadataujoinThreads. - Urutan keluaran program konkuren ditegakkan pada sisi pembacaan, bukan pada urutan peluncuran tugas.
Referensi#
- threadpool — Nim documentation
— Kontrak
spawn,FlowVar,^, dansyncsebagai penghalang penunggu seluruh tugas.