Fiber dan spawn

Fiber sebagai green thread kooperatif yang ringan dan spawn untuk membuatnya.

Daftar isi

Fiber adalah unit konkurensi paling dasar di Crystal, yaitu unit eksekusi ringan yang dikelola oleh runtime bahasa. Topik ini membahas cara spawn menjadwalkan fiber, sifat penjadwalan kooperatifnya, cara memberi nama fiber, dan pola penungguan berbasis channel agar program utama tidak berhenti sebelum fiber selesai. Setiap perilaku diilustrasikan dengan program yang dapat dijalankan beserta keluaran aslinya.

Fiber sebagai green thread#

Fiber adalah green thread, yaitu unit eksekusi yang dikelola oleh runtime Crystal sendiri, bukan oleh penjadwal sistem operasi. Satu thread sistem operasi dapat membawa banyak fiber, sehingga membuat fiber jauh lebih murah daripada membuat thread baru. Program dapat memuat ribuan fiber sekaligus tanpa membebani sistem operasi, dan biaya pergantian antar fiber juga jauh lebih kecil daripada pergantian thread. Berbeda dengan thread sistem operasi yang dapat dipotong kapan saja oleh penjadwalnya, pergantian fiber sepenuhnya berada di tangan runtime Crystal.

Fiber bekerja secara kooperatif. Setiap fiber berjalan terus sampai mencapai titik gantung (suspension point), yaitu posisi ketika fiber menyerahkan giliran kepada fiber lain. Titik gantung yang lazim ditemui adalah sleep, operasi I/O, dan Channel#receive. Fiber tidak pernah dipotong paksa di tengah perhitungan, sehingga kode di antara dua titik gantung selalu berjalan utuh tanpa disisipi fiber lain. Sifat ini menjelaskan hampir semua perilaku pada bagian berikut, mulai dari program yang berhenti terlalu dini sampai urutan keluaran yang mudah diprediksi.

Menjadwalkan fiber dengan spawn#

Metode spawn menjadwalkan fiber baru untuk dijalankan dan langsung mengembalikan kendali ke pemanggilnya. Blok yang menyertainya menjadi isi fiber tersebut, sedangkan kode setelah spawn tetap berjalan tanpa menunggu fiber selesai. Metode ini mengembalikan objek Fiber yang mewakili fiber baru, sehingga fiber dapat disimpan ke dalam variabel untuk keperluan tertentu. Parameter opsional seperti name juga diterima pada saat pemanggilan dan dibahas pada bagian tersendiri. Program berikut menjadwalkan satu fiber latar yang tidur selama 200 milidetik sebelum mencetak satu baris.

crystal tanpa-tunggu.cr
spawn do
  sleep 200.milliseconds
  puts "tugas latar selesai"
end

puts "program utama berhenti"
bash
# menjalankan program fiber
crystal run tanpa-tunggu.cr
text Keluaran
program utama berhenti

Keluaran hanya memuat satu baris dari program utama. Fiber latar baru saja dijadwalkan ketika program utama mencetak barisnya dan berhenti, sedangkan berhentinya program utama mengakhiri seluruh proses beserta semua fiber yang belum selesai. Fiber pada program di atas bahkan tidak pernah mencapai puts-nya karena masih menunggu sleep ketika proses berakhir. spawn memang tidak menyediakan penungguan otomatis, sehingga program memerlukan mekanisme tersendiri apabila hasil fiber harus tetap diperoleh, dan mekanisme itu dibahas pada bagian akhir topik ini.

Penjadwalan kooperatif#

Sifat kooperatif membuat urutan berjalannya dua fiber mudah diprediksi selama tidak ada titik gantung di antaranya. Dua fiber berikut masing-masing mencetak tiga langkah tanpa pernah menyerahkan giliran.

crystal tanpa-titik-gantung.cr
spawn do
  3.times { |i| puts "tugas A langkah #{i}" }
end

spawn do
  3.times { |i| puts "tugas B langkah #{i}" }
end

sleep 10.milliseconds
bash
# menjalankan program dua fiber tanpa titik gantung
crystal run tanpa-titik-gantung.cr
text Keluaran
tugas A langkah 0
tugas A langkah 1
tugas A langkah 2
tugas B langkah 0
tugas B langkah 1
tugas B langkah 2

Seluruh langkah tugas A ditampilkan lebih dulu, baru seluruh langkah tugas B. Fiber A berjalan sampai selesai karena tidak pernah mencapai titik gantung, dan fiber B baru mendapat giliran setelah fiber A berakhir. Urutan A lebih dulu mengikuti urutan pemanggilan spawn, bukan kebetulan waktu. Tidak ada pemotongan paksa di tengah perhitungan seperti yang terjadi pada thread sistem operasi.

Metode kelas Fiber.yield memaksa fiber yang sedang berjalan menyerahkan giliran dan masuk kembali ke antrean penjadwal. Dengan menyisipkan Fiber.yield di antara langkah-langkahnya, dua fiber dapat bergantian secara teratur tanpa menunggu I/O.

crystal giliran-fiber.cr
spawn do
  3.times do |i|
    puts "fiber: langkah #{i}"
    Fiber.yield
  end
end

3.times do |i|
  puts "utama: langkah #{i}"
  Fiber.yield
end
bash
# menjalankan program pergantian giliran dengan Fiber.yield
crystal run giliran-fiber.cr
text Keluaran
utama: langkah 0
fiber: langkah 0
utama: langkah 1
fiber: langkah 1
utama: langkah 2
fiber: langkah 2

Keluaran kini berselang-seling antara fiber utama dan fiber yang dijadwalkan. Setiap Fiber.yield mengembalikan fiber pemanggil ke antrean, sehingga penjadwal memilih fiber lain untuk berjalan dan giliran berpindah. Fiber utama pun dapat memanggil Fiber.yield karena penjadwal memperlakukannya sebagai salah satu fiber biasa.

Menamai fiber#

Parameter name pada spawn memberi nama pada fiber yang dibuat. Nama membantu saat membaca log atau menelusuri perilaku program karena setiap keluaran dapat dikaitkan dengan fiber asalnya. Pada program dengan banyak fiber yang menjalankan tugas serupa, nama membuat keluaran dapat dipilah tanpa menambah struktur data tambahan. Di dalam blok fiber, metode kelas Fiber.current mengembalikan objek fiber yang sedang berjalan, sehingga nama fiber dibaca melalui Fiber.current.name.

crystal fiber-nama.cr
spawn name: "tugas-laporan" do
  puts "fiber berjalan dengan nama #{Fiber.current.name}"
end

sleep 10.milliseconds
bash
# menjalankan program fiber bernama
crystal run fiber-nama.cr
text Keluaran
fiber berjalan dengan nama tugas-laporan

Nama bersifat opsional. Fiber yang tidak diberi nama melaporkan nil sebagai namanya, dan penamaan hanya dapat dilakukan pada saat fiber dibuat melalui parameter name. Objek yang dikembalikan spawn menyimpan nama yang sama, sehingga nama dapat dibaca langsung dari objek fiber tersebut. Penamaan tidak mengubah perilaku penjadwalan sama sekali, sehingga fiber bernama dan fiber anonim diperlakukan identik. sleep di akhir program menjaga program utama tetap hidup sampai fiber selesai mencetak, yaitu penungguan paling sederhana yang disempurnakan pada bagian berikutnya.

Karena spawn tidak menunggu, program memerlukan mekanisme tersendiri agar fiber latar sempat menyelesaikan tugasnya. Pola yang umum menggunakan channel, yaitu jalur kirim-terima nilai antar fiber. Channel untuk keperluan ini biasa dinamai done. Setiap fiber mengirim satu nilai ke channel tersebut ketika tugasnya selesai, sedangkan program utama menerima sebanyak jumlah fiber. Penerimaan yang memblokir inilah yang menjaga program tetap hidup sampai semua fiber selesai.

crystal tunggu-done.cr
done = Channel(Nil).new

4.times do
  spawn do
    sleep 20.milliseconds
    done.send(nil)
  end
end

sisa = 3
4.times do
  done.receive
  puts "satu fiber selesai, tersisa #{sisa}"
  sisa -= 1
end

puts "semua fiber selesai"
bash
# menjalankan program penunggu empat fiber
crystal run tunggu-done.cr
text Keluaran
satu fiber selesai, tersisa 3
satu fiber selesai, tersisa 2
satu fiber selesai, tersisa 1
satu fiber selesai, tersisa 0
semua fiber selesai

Jumlah penerimaan pada program utama harus sama dengan jumlah fiber yang dijadwalkan. Penerimaan yang kurang membuat beberapa fiber tidak sempat dilaporkan, sedangkan penerimaan berlebih membuat program utama menunggu tanpa akhir. Channel done bertipe Channel(Nil) karena nilainya tidak dibutuhkan, hanya kejadian selesainya yang penting, sedangkan channel bertipe lain dapat membawa hasil kerja setiap fiber, misalnya angka yang dihitung.

Setiap done.receive memblokir program utama sampai satu fiber mengirim, sehingga baris satu fiber selesai tercetak empat kali sesuai jumlah fiber. Setelah empat penerimaan tuntas, program utama mencetak baris terakhir dan berhenti dengan aman. Konsep channel dibahas lebih rinci pada topik tersendiri dalam modul ini.

Kesalahan umum#

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

  • Program utama berhenti sebelum fiber selesai. Penyebabnya, spawn tidak menunggu dan berhentinya program utama mengakhiri seluruh proses. Perbaikannya, terapkan pola channel done sehingga program utama hanya berhenti setelah seluruh fiber melapor.
  • Menganggap fiber berjalan paralel. Seluruh fiber berbagi satu thread dan bergantian hanya pada titik gantung, sehingga pekerjaan perhitungan murni tidak menjadi lebih cepat ketika dibagi ke banyak fiber. Dua fiber pada bagian penjadwalan kooperatif selalu berjalan bergantian dan tidak pernah serempak. Pembagian tugas tetap menguntungkan untuk pekerjaan yang sering menunggu I/O.
  • Mengurutkan keluaran dengan sleep. Memperkirakan lama tidur setiap fiber untuk menentukan urutan cetak adalah balapan melawan penjadwal, dan hasilnya tidak dapat diandalkan. Perbaikannya, kirim hasil melalui channel dan cetak dari satu tempat, misalnya dari fiber utama.

Ringkasan#

  • Fiber adalah green thread kooperatif yang dikelola runtime Crystal, dan spawn menjadwalkan fiber baru tanpa menunggu fiber selesai.
  • Fiber hanya bergantian pada titik gantung seperti sleep, operasi I/O, dan Channel#receive, sedangkan Fiber.yield menyerahkan giliran secara eksplisit.
  • Parameter name pada spawn menamai fiber, dan nama fiber dibaca melalui Fiber.current.name.
  • Berhentinya program utama mengakhiri semua fiber, sehingga program menunggu dengan pola channel done yang menerima satu nilai dari setiap fiber.

Referensi#