Thread

Peluncuran thread dengan spawn, penungguan joinThread, dan proc khusus thread.

Daftar isi

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 ^.

nim jumlah_spawn.nim
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()
bash
# kompilasi program spawn
nim c --warning[Deprecated]:off --hints:off jumlah_spawn.nim
# jalankan program hasil kompilasi
./jumlah_spawn
text Keluaran
bagian 0: 5525
bagian 1: 37400
bagian 2: 100525
bagian 3: 194900
total: 338350

Keluaran 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.

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.

nim jumlah_sync.nim
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
bash
# kompilasi program penungguan sync
nim c --warning[Deprecated]:off --hints:off jumlah_sync.nim
# jalankan program hasil kompilasi
./jumlah_sync
text Keluaran
bagian 0: 5525
bagian 1: 37400
bagian 2: 100525
bagian 3: 194900
total: 338350

Keluarannya 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.

Jalur 1: hasil melalui FlowVarThread utamaspawn tugasKolam threadFlowVar[int]^ membacakerja di thread lainJalur 2: hasil melalui slot bersamaspawn menulis ke slot ptr masing-masing tugas, lalu sync menunggu semuanya selesai
Dua jalur hasil dari tugas thread kembali ke thread utama

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.

nim Bentuk umum Thread dan joinThread
# 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 spawn sama 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 setelah sync.
  • Melupakan penungguan. Program yang tidak memanggil sync atau joinThread dapat 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#

  • spawn menjalankan panggilan proc di thread lain dan mengembalikan FlowVar[T], sedangkan ^ menunggu sampai nilai hasilnya tersedia.
  • sync menunggu seluruh tugas yang pernah diluncurkan, dan pola slot per thread melalui ptr menjaga setiap thread menulis ke lokasinya sendiri.
  • Proc khusus thread ditandai {.thread.}, diluncurkan dengan createThread, dan ditunggu dengan joinThread atau joinThreads.
  • Urutan keluaran program konkuren ditegakkan pada sisi pembacaan, bukan pada urutan peluncuran tugas.

Referensi#