Pernyataan select

Menunggu banyak channel sekaligus dengan select dan opsi timeout.

Daftar isi

Pernyataan select menunggu peristiwa pada beberapa channel sekaligus, lalu menjalankan cabang pertama yang siap. Topik ini membahas struktur select dengan beberapa cabang when, cabang else untuk keadaan tidak ada cabang yang siap, cabang timeout untuk membatasi lama penungguan, serta kesalahan yang lazim terjadi ketika select baru pertama kali digunakan. Setiap perilaku diilustrasikan dengan program yang dijalankan dan keluaran aslinya dicantumkan apa adanya.

Tanpa select, program yang menunggu beberapa channel memerlukan satu fiber penerima untuk setiap channel, sehingga koordinasi hasilnya tersebar di beberapa tempat. Pernyataan select menghimpun seluruh penungguan itu dalam satu blok. Strukturnya terdiri atas kata select, satu cabang when untuk setiap channel yang ikut ditunggu, dan penutup end. Nilai yang diterima dapat ditampung dengan penugasan pada kondisi cabang, misalnya pesan = channel.receive, sehingga isi blok cabang dapat menggunakan nilai tersebut.

Program berikut menunggu dua channel sensor bertipe Channel(Int32). Program menjalankan select tiga kali pada keadaan yang berbeda, yaitu ketika hanya channel pertama yang berisi nilai, ketika hanya channel kedua yang berisi nilai, dan ketika keduanya berisi nilai pada saat yang sama.

crystal select-dua-channel.cr
suhu = Channel(Int32).new(1)
kelembapan = Channel(Int32).new(1)

suhu.send(28)
select
when nilai = suhu.receive
  puts "sensor suhu: #{nilai}"
when nilai = kelembapan.receive
  puts "sensor kelembapan: #{nilai}"
end

kelembapan.send(70)
select
when nilai = suhu.receive
  puts "sensor suhu: #{nilai}"
when nilai = kelembapan.receive
  puts "sensor kelembapan: #{nilai}"
end

suhu.send(29)
kelembapan.send(71)
select
when nilai = suhu.receive
  puts "sensor suhu: #{nilai}"
when nilai = kelembapan.receive
  puts "sensor kelembapan: #{nilai}"
end
bash
# menjalankan program select dengan dua channel
crystal run select-dua-channel.cr
text Keluaran
sensor suhu: 28
sensor kelembapan: 70
sensor suhu: 29

Pada ronde pertama dan kedua, hanya satu channel yang berisi nilai ketika select dieksekusi, sehingga cabang channel itu yang dijalankan dan cabang lainnya dilewati. Pada ronde ketiga, kedua channel sama-sama siap, dan Crystal memeriksa cabang sesuai urutan penulisannya, sehingga cabang suhu.receive yang terpilih. select mengeksekusi satu cabang saja per pernyataan, sedangkan nilai yang belum diambil tetap menunggu di channelnya masing-masing. Dua ronde pertama sekaligus membuktikan bahwa cabang yang menunggu channel kosong tidak menghalangi cabang lain yang sudah siap.

Cabang else untuk pemeriksaan tanpa menunggu#

Cabang else dijalankan segera apabila tidak ada satu pun cabang when yang siap pada saat select dieksekusi. Pernyataan select yang memiliki cabang else karenanya tidak pernah memblokir, sekalipun seluruh channelnya dalam keadaan kosong. Pola ini digunakan untuk mengosongkan antrean selama masih ada isinya, lalu melakukan langkah lain ketika antrean telah kosong.

Program berikut mengisi channel berbuffer dengan tiga nilai, kemudian menjalankan select sebanyak empat kali di dalam perulangan. Tiga iterasi pertama menemukan nilai, sedangkan iterasi keempat menemukan antrean yang sudah kosong.

crystal select-perulangan.cr
antrean = Channel(String).new(3)
antrean.send("berkas a")
antrean.send("berkas b")
antrean.send("berkas c")

4.times do
  select
  when tugas = antrean.receive
    puts "memproses #{tugas}"
  else
    puts "antrean kosong"
  end
end
bash
# menjalankan program select dengan cabang else
crystal run select-perulangan.cr
text Keluaran
memproses berkas a
memproses berkas b
memproses berkas c
antrean kosong

Iterasi keempat adalah bukti perilaku else sekaligus alasan keberadaannya. Tanpa else, pemanggilan antrean.receive yang keempat memblokir program selamanya karena tidak akan ada nilai baru. Dengan else, keadaan kosong terdeteksi seketika dan program melanjutkan langkah berikutnya. Kombinasi select dan else di dalam perulangan juga menjadi cara sederhana untuk memeriksa ketersediaan data secara berkala tanpa menyerahkan kendali selamanya kepada satu channel.

Batas waktu dengan timeout#

Metode timeout(durasi) digunakan sebagai cabang select yang menjadi siap setelah durasi yang diberikan berlalu, tanpa memerlukan require tambahan. Cabang ini menjawab kebutuhan yang tidak dapat dijawab else, yaitu memberi kesempatan kepada channel untuk menerima data, tetapi tetap membatasi lama penungguannya. Program berikut memperlihatkan kedua keadaannya dalam satu berkas, yaitu channel yang diam total dan channel yang menerima pesan sebelum batas waktu tercapai.

crystal select-timeout.cr
lambat = Channel(String).new

select
when pesan = lambat.receive
  puts "pesan diterima: #{pesan}"
when timeout(50.milliseconds)
  puts "waktu tunggu habis, tidak ada pesan"
end

pengirim = Channel(String).new
spawn do
  sleep 50.milliseconds
  pengirim.send("datang tepat waktu")
end

select
when pesan = pengirim.receive
  puts "pesan diterima: #{pesan}"
when timeout(5.seconds)
  puts "waktu tunggu habis, tidak ada pesan"
end
bash
# menjalankan program select dengan batas waktu
crystal run select-timeout.cr
text Keluaran
waktu tunggu habis, tidak ada pesan
pesan diterima: datang tepat waktu

Channel lambat tidak pernah menerima apa pun, sehingga setelah 50 milidetik cabang timeout menjadi siap dan dijalankan. Pada ronde kedua, fiber pengirim tidur selama 50 milidetik sebelum mengirim, sedangkan batas waktunya 5 detik, sehingga cabang receive menang jauh sebelum timeout sempat siap. Perbandingan kedua ronde menunjukkan bahwa pemenang select ditentukan oleh peristiwa mana yang terjadi lebih dulu, bukan oleh urutan penulisan cabang ketika hanya satu cabang yang siap.

Kesalahan umum#

Tiga kesalahan berikut lazim muncul ketika select baru pertama kali digunakan.

  • Mengira select menunggu semua channel sekaligus tuntas. select hanya menunggu sampai satu cabang siap, lalu menjalankan cabang itu dan selesai. Apabila seluruh nilai harus dikumpulkan, terapkan select di dalam perulangan dan hitung jumlah penerimaan, atau gunakan satu fiber penerima per channel.
  • Menulis select tanpa else maupun timeout untuk data yang belum pasti datang. Pernyataan semacam itu menunggu tanpa batas apabila pengirimnya gagal berjalan. Bungkus penungguan dengan cabang timeout agar program dapat melapor dan melanjutkan langkah lain.
  • Menganggap select melindungi dari channel yang sudah ditutup. Cabang receive pada channel yang telah ditutup tetap memicu error Channel::ClosedError, baik di dalam maupun di luar select. Tangani error tersebut dengan blok rescue, atau gunakan receive? yang mengembalikan nil.

Ringkasan#

  • Pernyataan select menunggu beberapa channel sekaligus dan menjalankan tepat satu cabang when, yaitu cabang yang channelnya siap lebih dulu.
  • Apabila lebih dari satu cabang siap pada saat yang sama, Crystal memeriksa cabang sesuai urutan penulisannya dan memilih cabang siap yang pertama.
  • Cabang else dijalankan segera ketika tidak ada cabang yang siap, sehingga select yang memilikinya tidak pernah memblokir.
  • Cabang timeout(durasi) menjadi siap setelah durasi berlalu dan menjadi cara baku untuk membatasi lama penungguan.

Referensi#