Topik ini membahas anotasi gcsafe dan disiplin memori bersama pada program konkuren Nim. Pembahasan mencakup makna gcsafe sebagai kontrak bahwa sebuah proc tidak menyentuh memori milik pengelola sampah dari thread lain, bukti bahwa kompilator menolak pelanggarannya saat kompilasi, serta cara yang sah untuk berbagi penghitung antar thread melalui ptr dan operasi atomik dari modul std/atomics. Setelah topik ini, batas antara yang boleh dan yang ditolak lintas thread dapat dibaca langsung dari pesan error kompilator.
Makna anotasi gcsafe#
gcsafe adalah kontrak bahwa sebuah proc tidak mengakses memori yang dikelola pengelola sampah (garbage collector) secara global, yaitu memori objek ref, string, dan seq yang dihitung dan dibebaskan oleh pengelola sampah. Alasannya bersifat teknis: satu objek yang dirujuk oleh dua thread membuat penghitungan rujukan dan pemusnahan objek menjadi ambigu, dan struktur data seperti seq dapat direalokasi oleh satu thread pada saat thread lain sedang membacanya. Nim 2.x mengaktifkan dukungan thread secara bawaan, sehingga pemeriksaan ini bekerja tanpa sakelar tambahan.
Anotasi {.gcsafe.} dapat ditulis secara eksplisit pada sebuah proc, tetapi ia juga menyertai proc yang ditandai {.thread.}. Titik pemeriksaan yang paling sering ditemui adalah spawn dari modul std/threadpool, karena spawn hanya menerima panggilan proc yang gcsafe. Pemeriksaannya terjadi pada saat kompilasi, sehingga pelanggaran tidak pernah lolos sampai ke program yang sedang berjalan. Pendekatan ini membuat batas keamanan menjadi bagian dari tipe program, bukan kesepakatan yang hanya tertulis di komentar.
Pelanggaran yang ditolak kompilator#
Contoh berikut sengaja melanggar kontrak itu. Variabel global catatan bertipe string, dan string pada Nim memakai memori yang dikelola pengelola sampah. Proc kerja membacanya, sehingga kerja bukan proc gcsafe, dan pemanggilan spawn kerja() ditolak.
# tidak-dijalankan
import std/threadpool
var catatan: string = "data terkini"
proc kerja(): int =
echo catatan
0
discard spawn kerja()
sync()
echo "selesai"Kompilasi contoh di atas berhenti dengan pesan error berikut, yang diambil dari kompilasi nyata pada baris pemanggilan spawn.
Error: 'spawn' takes a GC safe call expressionPesan error tersebut tidak menyebut variabelnya secara langsung, sebab pemeriksaan bekerja pada tingkat proc: kerja dinilai tidak gcsafe karena membaca global bertipe string, dan spawn menolak seluruh panggilannya. Perbaikannya bukan menekan error, melainkan mengubah jalur datanya, misalnya mengirim isi catatan sebagai argumen tugas atau mengirimkannya sebagai pesan melalui channel. Kedua cara itu memindahkan nilai, bukan berbagi memori yang sama.
Memori bersama yang sah#
Cara yang sah untuk berbagi memori antar thread adalah memori mentah yang tidak dikelola pengelola sampah, diakses melalui ptr, dengan operasi baca-tulis yang dijamin tidak terpotong. Modul std/atomics menyediakan tipe Atomic[T] untuk keperluan itu. Proc fetchAdd melakukan penambahan secara atomik, yaitu membaca, menambah, dan menulis sebagai satu langkah yang tidak dapat disisipi thread lain, sedangkan store dan load menulis dan membaca nilainya. Tipe Atomic[int] sendiri adalah objek biasa tanpa memori pengelola sampah, sehingga ia boleh diakses dari tugas yang diluncurkan spawn.
import std/[atomics, threadpool]
var penghitung: Atomic[int]
proc naikkan(jumlah: int) =
for i in 1 .. jumlah:
discard penghitung.fetchAdd(1)
penghitung.store(0)
for i in 0 .. 3:
spawn naikkan(100_000)
sync()
echo "total: ", penghitung.load# kompilasi program penghitung atomik
nim c --warning[Deprecated]:off --hints:off penghitung_atomik.nim
# jalankan program hasil kompilasi
./penghitung_atomiktotal: 400000Keluarannya selalu 400000, yaitu empat tugas dikali seratus ribu penambahan, dan nilainya sama pada setiap eksekusi. Kepastian itu berasal dari sifat atomik fetchAdd: setiap penambahan terhitung lengkap sebelum penambahan berikutnya mulai, sehingga tidak ada pembaruan yang hilang walau keempat tugas berjalan bersamaan. Bandingkan dengan pola yang tampak serupa pada variabel global biasa, yang penambahannya terdiri atas tiga langkah terpisah dan akibatnya saling menimpa.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika memori bersama di Nim baru dipraktikkan.
- Mengganti
Atomic[int]dengan variabel global biasa. Penambahanpenghitung = penghitung + 1adalah tiga langkah, yaitu baca, tambah, dan tulis, dan dua thread yang melakukannya bersamaan dapat menimpa pembaruan satu sama lain. Jumlah akhirnya menjadi kurang dari yang diharapkan dan berubah antar eksekusi, sehingga tidak layak dijadikan keluaran yang dapat direproduksi. Perbaikannya adalahfetchAddseperti pada contoh, atau slot terpisah untuk setiap thread yang dijumlahkan setelahsync. - Mengira
{.gcsafe.}adalah pengaman saat program berjalan. Anotasi itu adalah janji yang diperiksa pada saat kompilasi, dan pemeriksaan hanya sekuat analisis yang mampu dilakukan kompilator. Keamanan sesungguhnya tetap ditentukan oleh pola akses datanya, misalnya slot per thread atau operasi atomik. - Membagikan objek bertipe
string,seq, ataurefantar thread dengan berbagai cara untuk melewati error kompilasi. Objek semacam itu berada di memori yang dihitung pengelola sampah, dan dua thread yang merujuk objek yang sama menimbulkan perilaku yang tidak terdefinisi. Pindahkan nilainya melalui channel, sehingga satu objek hanya dimiliki satu thread pada satu waktu.
Ringkasan#
gcsafeadalah kontrak bahwa sebuah proc tidak mengakses memori milik pengelola sampah dari thread lain, disiratkan oleh{.thread.}, dan dituntut olehspawn.- Pelanggaran ditolak pada saat kompilasi dengan pesan error
'spawn' takes a GC safe call expression, bukan pada saat program berjalan. - Memori bersama yang sah memakai
ptrbeserta operasi atomik sepertifetchAdd, sehingga jumlah akhir tidak bergantung pada urutan eksekusi thread. - Variabel global biasa yang ditambah dari beberapa thread mengalami persaingan data (data race) dan hasilnya berubah antar eksekusi.
Referensi#
- atomics — Nim documentation
— Kontrak
Atomic[T],fetchAdd,load, danstoresebagai operasi baca-tulis yang tidak terpotong antar thread. - threadpool — Nim documentation
— Syarat ekspresi panggilan gcsafe pada
spawnyang menolak contoh pelanggaran di atas.