Channel

Pengiriman pesan bertipe yang aman antar-thread melalui channel.

Daftar isi

Topik ini membahas channel sebagai saluran pertukaran pesan bertipe antar thread pada Nim. Pembahasan mencakup semantik antrean dari Channel[T], pola tugas-jawab, yaitu thread utama mengirim pekerjaan dan thread pekerja mengirim kembali hasil, serta pembacaan tanpa menunggu melalui tryRecv. Berbeda dari berbagi variabel langsung, komunikasi lewat channel tidak menimbulkan risiko dua thread menulis ke memori yang sama, karena setiap pesan berpindah utuh dari pengirim ke penerima.

Channel sebagai antrean pesan bertipe#

Channel[T] adalah antrean pesan (message queue) yang menyimpan nilai bertipe T dengan urutan keluar-masuk yang terjaga, yaitu pesan yang dikirim lebih dulu akan diterima lebih dulu. Tipe Channel tersedia dari sistem bawaan Nim ketika dukungan thread aktif, yang pada Nim 2.x merupakan perilaku bawaan kompilasi. Proc send menaruh satu pesan di ekor antrean, sedangkan recv mengambil satu pesan dari kepalanya, dan bila antrean sedang kosong, recv menunggu sampai ada pesan yang datang. Satu pesan hanya diambil oleh satu penerima, sehingga dua thread yang sama-sama membaca satu channel tidak pernah menerima pesan yang sama.

Karena satu pesan hanya diambil satu penerima, channel juga berfungsi sebagai alat pembagi kerja. Sejumlah thread pekerja dapat membaca satu channel tugas yang sama, dan setiap tugas diproses tepat sekali oleh salah satu pekerja tanpa pengunci tambahan. Pemisahan peran ini mengikuti gaya pengiriman pesan (message passing), yaitu thread tidak saling menyentuh memori, melainkan bertukar data melalui antrean yang dikelola sistem. Pola seperti ini menghindarkan program dari persaingan data (data race) karena satu-satunya jalur perpindahan data adalah pesan yang disalin utuh ke penerimanya.

Sebelum dipakai, sebuah channel wajib disiapkan dengan open. Proc open menerima argumen opsional maxItems untuk membatasi kapasitas, sedangkan tanpa argumen, antreannya tidak berbatas. Proc close menandai akhir pemakaian dan melepaskan sumber daya antreannya. Karena Channel[T] adalah tipe nilai yang tidak memakai memori pengelola sampah, objek channel boleh diletakkan sebagai variabel global dan diakses dari proc yang dijalankan di thread lain, misalnya tugas yang diluncurkan dengan spawn dari modul std/threadpool.

nim Contoh: deklarasi dan siklus hidup channel
# tidak-dijalankan
var kanal: Channel[string]

kanal.open()
kanal.send("pesan pertama")
let isi = kanal.recv()
kanal.close()

Pola tugas-jawab antar thread#

Pola yang paling sering dipakai adalah tugas-jawab (request-response). Thread utama mengirim serangkaian permintaan ke satu channel, thread pekerja membaca permintaan itu dalam perulangan, mengolahnya, lalu mengirim jawaban ke channel kedua. Pekerjaan dihentikan dengan pesan penanda (sentinel), yaitu nilai khusus yang disepakati bermakna “tidak ada pekerjaan lagi”. Setelah thread utama menunggu pekerja selesai, baru channel jawaban dikosongkan. Urutan pengerjaan ini menegakkan determinisme: seluruh jawaban sudah pasti berada di antrean pada saat pembacaan dimulai.

nim kanal_kuadrat.nim
import std/threadpool

type
  Jawaban = tuple
    id: int
    nilai: int

var kotakMasuk: Channel[int]
var kotakJawab: Channel[Jawaban]

proc penghitungKuadrat(): int =
  while true:
    let pesan = kotakMasuk.recv()
    if pesan < 0:
      return 0
    kotakJawab.send((id: pesan, nilai: pesan * pesan))

kotakMasuk.open()
kotakJawab.open()
discard spawn penghitungKuadrat()
for i in 1 .. 4:
  kotakMasuk.send(i)
kotakMasuk.send(-1)
sync()

var hasil: array[4, int]
while true:
  let (ada, pesan) = kotakJawab.tryRecv()
  if not ada:
    break
  hasil[pesan.id - 1] = pesan.nilai
kotakMasuk.close()
kotakJawab.close()
for i in 0 .. 3:
  echo "kuadrat ", i + 1, " = ", hasil[i]
bash
# kompilasi program tugas-jawab channel
nim c --warning[Deprecated]:off --hints:off kanal_kuadrat.nim
# jalankan program hasil kompilasi
./kanal_kuadrat
text Keluaran
kuadrat 1 = 1
kuadrat 2 = 4
kuadrat 3 = 9
kuadrat 4 = 16

Program di atas mengirim angka 1 sampai 4 ditambah penanda -1, dan pekerja menjawab dengan tuple (id, nilai) berisi identitas permintaan beserta kuadratnya. Jawaban tidak dicetak begitu diterima, melainkan ditempatkan ke elemen hasil[pesan.id - 1], sehingga urutan cetak mengikuti id, bukan urutan kedatangan pesan. Penempatan berdasarkan identitas seperti ini menjaga keluaran tetap sama pada setiap eksekusi walaupun kecepatan tiap thread berubah-ubah. sync memastikan pekerja sudah berhenti sebelum antrean jawaban dikosongkan, sehingga perulangan tryRecv menemukan tepat empat jawaban.

Thread utamaChannel[int]Thread pekerjaChannel[Jawaban]send 1..4, -1recvsend (id, nilai)tryRecvkosongkan setelah sync
Alur tugas-jawab: pekerjaan dikirim lewat Channel[int], hasil dikembalikan lewat Channel[Jawaban], lalu antrean jawaban dikosongkan dengan tryRecv setelah sync

Membaca tanpa menunggu dengan tryRecv#

recv yang menunggu tidak selalu diinginkan, misalnya ketika thread utama perlu tetap bekerja sambil sesekali memeriksa antrean. Untuk kebutuhan itu, tryRecv mencoba mengambil satu pesan tanpa pernah menunggu. Nilai kembaliannya adalah tuple dengan dua bidang, yaitu dataAvailable yang bernilai true bila ada pesan yang berhasil diambil, dan msg yang berisi pesan itu. Bila antrean kosong, dataAvailable bernilai false dan msg berisi nilai default tipe pesan, sehingga bidang msg hanya boleh dipercaya setelah dataAvailable diperiksa.

nim Contoh: pembacaan tanpa menunggu
# tidak-dijalankan
let (ada, pesan) = kotakJawab.tryRecv()
if ada:
  echo "diterima: ", pesan
else:
  echo "antrean kosong"

Pola perulangan while true dengan tryRecv pada contoh program memakai tuple ini sebagai alat pengosongan (drain), yaitu mengambil semua pesan yang tersisa sampai antrean benar-benar kosong. Pola serupa dapat dipakai untuk memeriksa jawaban secara berkala di tengah pekerjaan lain, asalkan pesan yang belum diambil tetap tersimpan di antrean karena channel tidak membuang isi antreannya sendiri. Pengosongan lazim dilakukan setelah sync karena pada saat itu seluruh jawaban sudah masuk antrean, sehingga perulangan cukup berhenti pada percobaan pertama yang mengembalikan dataAvailable bernilai false, dan tidak ada pesan yang tertinggal tanpa diproses.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika channel baru dipraktikkan.

  • Lupa memanggil open sebelum channel dipakai. Tanpa open, perilaku send dan recv tidak terdefinisi dengan baik dan program bisa menggantung. Siapkan open berdampingan dengan deklarasi pemakaiannya.
  • Menganggap channel berbagi memori dengan pengirimnya. send menyalin nilai pesan ke antrean, sehingga perubahan pada variabel asal setelah pengiriman tidak terlihat oleh penerima. Rancang pesan sebagai nilai yang berdiri sendiri.
  • Menunggu pesan yang tidak pernah dikirim. recv pada antrean kosong menunggu tanpa batas, dan kelalaian satu sisi membuat pasangannya menggantung selamanya. Gunakan penanda akhir seperti nilai -1 pada contoh, atau alihkan ke tryRecv bila ketiadaan pesan adalah keadaan yang wajar.

Ringkasan#

  • Channel[T] adalah antrean pesan bertipe dengan urutan keluar-masuk yang terjaga, wajib disiapkan dengan open, dan diakhiri dengan close.
  • send menyalin pesan ke ekor antrean, recv mengambil dari kepala dan menunggu bila kosong, sedangkan tryRecv mengembalikan tuple (dataAvailable, msg) tanpa pernah menunggu.
  • Pola tugas-jawab yang deterministik memakai pesan penanda untuk menghentikan pekerja, menunggu dengan sync, lalu mengosongkan antrean jawaban dengan penempatan berdasarkan identitas pesan.

Referensi#