Execution context

Eksekusi fiber dalam execution context yang dapat paralel sejak Crystal 1.21.

Daftar isi

Sejak Crystal 1.21, fiber tidak lagi berjalan pada satu thread yang tetap, melainkan di dalam execution context, yaitu konteks eksekusi yang mengelola sekumpulan penjadwal tempat fiber-fiber dijalankan. Topik ini membahas konteks default yang menyertai setiap program, konteks paralel Fiber::ExecutionContext::Parallel, konteks konkuren Fiber::ExecutionContext::Concurrent, cara memperbesar paralelisme, serta perubahan perilaku same_thread yang menyertai fitur ini. Setiap program dijalankan dan keluaran aslinya dicantumkan apa adanya.

Konteks default setiap program#

Setiap program Crystal otomatis memulai satu execution context yang diperlihatkan sebagai Fiber::ExecutionContext.default, dan fiber utama berjalan di dalamnya. Konteks ini bertipe Parallel, tetapi dibuat dengan kapasitas satu demi kesesuaian perilaku dengan Crystal versi lama yang berjalan pada satu thread. Metode spawn tanpa argumen konteks menjadwalkan fiber baru ke konteks milik fiber saat ini, sehingga seluruh spawn pada contoh topik-topik sebelumnya sesungguhnya menjadwalkan fiber ke konteks default ini.

crystal ec-default.cr
konteks = Fiber::ExecutionContext.default
puts konteks.class
puts "kapasitas: #{konteks.capacity}"
puts "nama: #{konteks.name}"
bash
# menjalankan program konteks default
crystal run ec-default.cr
text Keluaran
Fiber::ExecutionContext::Parallel
kapasitas: 1
nama: DEFAULT

Metode capacity melaporkan jumlah penjadwal maksimum yang boleh dijalankan konteks, sedangkan nama DEFAULT menandai konteks bawaan runtime. Kapasitas satu berarti pekerjaan yang dijadwalkan dengan spawn biasa berjalan satu per satu, sekalipun fiber-fibernya bergantian secara aktif. Konteks default dapat diperbesar pada saat berjalan, dan pembesarannya dibahas pada bagian akhir topik ini.

Konteks paralel dengan Parallel#

Kelas Fiber::ExecutionContext::Parallel membuat konteks yang beranggotakan beberapa penjadwal, dan setiap penjadwal berjalan pada satu thread sistem operasi. Parameternya adalah nama konteks dan batas atas paralelismenya, misalnya Parallel.new("pengolah", 2) untuk konteks yang boleh menjalankan paling banyak dua fiber secara serempak. Paralelisme aktualnya dinamis, yaitu bertambah ketika antrean fiber penuh dan menyusut ketika kosong, tetapi tidak pernah melewati batas yang ditetapkan.

crystal ec-parallel.cr
ec = Fiber::ExecutionContext::Parallel.new("pengolah", 2)
puts "kapasitas konteks: #{ec.capacity}"
selesai = Channel(Nil).new

4.times do
  ec.spawn do
    selesai.send(nil)
  end
end

4.times { selesai.receive }
puts "empat tugas paralel selesai"
bash
# menjalankan program konteks paralel
crystal run ec-parallel.cr
text Keluaran
kapasitas konteks: 2
empat tugas paralel selesai

Pemanggilan ec.spawn menjadwalkan fiber ke konteks ec, bukan ke konteks default, sehingga paling banyak dua fiber di antara keempatnya berjalan secara serempak. Fiber pada konteks paralel tidak terikat pada satu thread, dan fiber yang digantungkan boleh dilanjutkan pada thread lain oleh penjadwalnya. Pola channel selesai digunakan kembali di sini agar program utama hanya berhenti setelah keempat fiber melapor, sebab urutan penyelesaian fiber pada konteks paralel tidak dapat dipastikan.

Konteks konkuren dengan Concurrent#

Kelas Fiber::ExecutionContext::Concurrent membuat konteks yang seluruh fibernya berbagi satu penjadwal, sehingga fiber-fibernya bergantian secara kooperatif dan tidak pernah berjalan paralel satu sama lain. Konteks ini tetap berjalan paralel terhadap konteks lain, termasuk konteks default. Karena tidak ada dua fiber yang aktif bersamaan, data yang dibagi antar fiber dalam konteks yang sama tidak memerlukan perlindungan khusus, dan sinkronisasinya menjadi lebih murah.

crystal ec-concurrent.cr
ec = Fiber::ExecutionContext::Concurrent.new("pengolah")
hasil = Channel({Char, Array(String)}).new

ec.spawn do
  baris = [] of String
  3.times { |i| baris << "tugas A langkah #{i}" }
  hasil.send({'A', baris})
end

ec.spawn do
  baris = [] of String
  3.times { |i| baris << "tugas B langkah #{i}" }
  hasil.send({'B', baris})
end

kumpulan = Hash(Char, Array(String)).new
2.times do
  huruf, baris = hasil.receive
  kumpulan[huruf] = baris
end

['A', 'B'].each do |huruf|
  kumpulan[huruf].each { |b| puts b }
end
puts "dua fiber konkuren selesai"
bash
# menjalankan program konteks konkuren
crystal run ec-concurrent.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
dua fiber konkuren selesai

Setiap fiber mengumpulkan hasilnya sendiri ke dalam array, mengirimkannya melalui channel, dan program utama mencetak hasil dalam urutan yang ditetapkannya sendiri. Pola ini menjaga keluaran tetap sama di setiap eksekusi meskipun urutan penjadwalan kedua fiber tidak dijamin. Konsekuensi berbagi satu penjadwal perlu diwaspadai, yaitu satu fiber yang memblokir tanpa titik gantung menghentikan seluruh fiber dalam konteks tersebut, sehingga pekerjaan berat sebaiknya dipindahkan ke konteks paralel.

Memperbesar paralelisme konteks default#

Nilai acuan jumlah pekerja konteks default dapat dibaca melalui metode kelas Fiber::ExecutionContext.default_workers_count. Metode ini menghormati variabel lingkungan CRYSTAL_WORKERS apabila diatur, dan apabila tidak, mengambil jumlah CPU logis yang tersedia. Nilai tersebut tidak otomatis diterapkan, sehingga pembesaran kapasitas dilakukan secara eksplisit melalui metode resize.

crystal ec-workers.cr
puts "jumlah pekerja yang disarankan: #{Fiber::ExecutionContext.default_workers_count}"
puts "kapasitas konteks default: #{Fiber::ExecutionContext.default.capacity}"
bash
# menjalankan program dengan dua pekerja pada variabel lingkungan
CRYSTAL_WORKERS=2 crystal run ec-workers.cr
text Keluaran
jumlah pekerja yang disarankan: 2
kapasitas konteks default: 1
crystal ec-resize.cr
konteks = Fiber::ExecutionContext.default
puts "kapasitas awal: #{konteks.capacity}"
konteks.resize(4)
puts "kapasitas baru: #{konteks.capacity}"
bash
# menjalankan program pembesaran kapasitas
crystal run ec-resize.cr
text Keluaran
kapasitas awal: 1
kapasitas baru: 4

Keluaran pertama memperlihatkan bahwa CRYSTAL_WORKERS=2 membuat default_workers_count melaporkan angka dua, sedangkan kapasitas konteks default tetap satu karena nilai variabel lingkungan tidak diterapkan secara otomatis. Pemisahan keduanya menegaskan pembagian peran, yaitu variabel lingkungan memberi nilai acuan, sedangkan keputusan pemakaiannya tetap berada di tangan program. Metode resize(4) pada program kedua memperbesar konteks default saat program berjalan, dan capacity langsung melaporkan nilai barunya. Setelah kapasitas diperbesar, fiber yang dijadwalkan dengan spawn biasa pun dapat berjalan serempak sampai batas tersebut.

same_thread tidak lagi didukung#

Sebelum Crystal 1.21, argumen same_thread: true pada spawn menjamin fiber berjalan pada thread yang sama dengan pembuatnya. Jaminan itu tidak dapat dipertahankan ketika fiber bebas berpindah thread antar penggantungan, sehingga argumen tersebut kini menimbulkan peringatan pengabaian pada saat kompilasi dan error RuntimeError tepat pada pemanggilan spawn di konteks paralel. Penolakan berlapis ini membuat kode yang mengandalkan same_thread terdeteksi sejak dini, bukan berjalan dengan perilaku yang berubah diam-diam.

crystal ec-same-thread.cr
begin
  spawn(same_thread: true) do
    puts "tidak sampai dieksekusi"
  end
rescue error : RuntimeError
  puts "spawn ditolak: #{error.message}"
end
bash
# menjalankan program penolakan same_thread
crystal run ec-same-thread.cr
text Keluaran
In ec-same-thread.cr:2:22

 2 | spawn(same_thread: true) do
                        ^
Warning: Deprecated argument same_thread. It will have no effect with execution contexts.

A total of 1 warnings were found.
spawn ditolak: Fiber::ExecutionContext::Parallel::Scheduler#spawn doesn't support same_thread:true

Keluaran di atas memperlihatkan dua lapisan perlindungan sekaligus, yaitu peringatan pengabaian dari compiler dan penolakan berupa error yang dapat ditangkap dengan rescue. Blok fiber pada program itu tidak pernah dijalankan, sehingga baris tidak sampai dieksekusi tidak pernah tercetak. Kode yang masih mengandalkan same_thread perlu ditinjau ulang, dan kelas Fiber::ExecutionContext::Isolated tersedia sebagai alternatif untuk fiber tunggal yang menguasai satu thread penuh selama masa hidupnya.

Kesalahan umum#

Tiga kesalahan berikut lazim muncul ketika fitur execution context baru pertama kali digunakan.

  • Mengira spawn biasa sudah berjalan paralel. Fiber pada konteks default berbagi kapasitas satu, sehingga tugas berat yang dipecah dengan spawn tetap berjalan satu per satu. Perbaikannya, buat konteks Parallel dengan kapasitas yang diinginkan atau perbesar konteks default dengan resize.
  • Menaruh pekerjaan panjang di konteks Concurrent. Seluruh fiber konteks itu menunggu satu fiber yang tidak pernah menyerahkan giliran. Perbaikannya, pindahkan pekerjaan tersebut ke konteks Parallel atau sisipkan Fiber.yield agar giliran berpindah.
  • Menganggap nilai capacity sebagai jumlah thread yang sedang aktif. capacity adalah batas atas paralelisme, sedangkan jumlah penjadwal yang benar-benar aktif naik dan turun mengikuti beban. Gunakan nilai itu sebagai batas desain, bukan sebagai penghitung thread.

Ringkasan#

  • Sejak Crystal 1.21, fiber berjalan di dalam execution context, dan spawn tanpa argumen menjadwalkan fiber ke konteks milik fiber saat ini.
  • Konteks default bertipe Parallel dengan kapasitas satu, dan kelas Concurrent menyediakan konteks satu thread yang kooperatif.
  • Kelas Parallel.new("nama", batas) membuka paralelisme hingga batas yang ditetapkan, dengan jumlah penjadwal aktif yang dinamis.
  • Jumlah pekerja acuan konteks default dibaca melalui default_workers_count yang menghormati CRYSTAL_WORKERS, sedangkan pembesaran aktual menggunakan resize.
  • Argumen same_thread: true sudah tidak didukung dan memicu peringatan kompilasi serta error pada saat penjadwalan.

Referensi#