Atom

Mengelola state independen sinkron dengan atom, swap!, dan validator.

Daftar isi

Topik ini membahas atom sebagai mekanisme state yang paling sering digunakan pada Clojure. Pembahasan mencakup pembuatan dan pembacaan atom, penggantian nilai dengan swap! dan reset!, semantik compare-and-set di baliknya, serta validator yang menjaga nilai tetap sah. Setelah topik ini selesai, state independen yang diganti secara sinkron dapat dikelola dengan pola yang aman terhadap konkurensi.

Membuat dan membaca atom#

Atom adalah identity yang nilai satu-satunya dapat diganti secara sinkron dan independen dari identitas lain. Pembuatan atom menggunakan fungsi atom, pembacaan nilai menggunakan deref atau tanda @, dan penggantian nilai menggunakan swap! atau reset!. Contoh berikut mengelola saldo kartu ujian dengan atom.

clojure Contoh: saldo kartu ujian pada atom
(def saldo (atom 50000))

(println "Saldo awal:" @saldo)
(println "Bentuk deref:" (deref saldo))

(swap! saldo + 15000)
(println "Setelah swap!:" @saldo)

(reset! saldo 40000)
(println "Setelah reset!:" @saldo)
text Keluaran
Saldo awal: 50000
Bentuk deref: 50000
Setelah swap!: 65000
Setelah reset!: 40000

Tanda @ adalah singkatan baca untuk deref, sehingga @saldo dan (deref saldo) menghasilkan nilai yang sama. Fungsi swap! menerapkan fungsi pengubah terhadap nilai saat ini, sedangkan reset! menetapkan nilai baru tanpa melihat nilai lama. Penimpaan tanpa syarat pada reset! mudah menghapus perhitungan yang sah, sehingga swap! menjadi pilihan bawaan ketika nilai baru diturunkan dari nilai lama.

swap! dengan fungsi murni#

swap! menghitung nilai baru dengan memanggil fungsi pengubah terhadap nilai atom saat ini, lalu memasang hasilnya sebagai nilai berikutnya. Fungsi pengubahnya wajib murni, yaitu keluarannya hanya ditentukan oleh argumennya dan tidak melakukan efek samping, karena swap! dapat mengulang pemanggilannya. Di balik layar, swap! bekerja dengan operasi compare-and-set, yakni pembandingan dan penggantian yang dijamin atomik. Apabila nilai atom berubah di antara pembacaan dan penggantian, pembandingan gagal dan seluruh proses diulang dengan nilai terbaru.

Perilaku pengulangan itu terlihat bila banyak thread bersaingan memperbarui atom yang sama. Fungsi yang menulis ke berkas atau mencetak ke layar dapat terpanggil lebih banyak kali daripada jumlah penggantian yang benar-benar terjadi.

Contoh berikut mengelola poin pelanggan perpustakaan. Setiap pelanggan direpresentasikan sebagai keyword, sedangkan seluruh poin ditampung dalam satu map pada satu atom.

clojure Contoh: poin pelanggan perpustakaan
(def poin (atom {:ali 0 :budi 0 :citra 0}))

(swap! poin update :ali + 10)
(swap! poin update :budi + 5)
(swap! poin update :citra inc)

(println "Poin Ali:" (:ali @poin))
(println "Poin Budi:" (:budi @poin))
(println "Poin Citra:" (:citra @poin))
text Keluaran
Poin Ali: 10
Poin Budi: 5
Poin Citra: 1

Pembaruan poin Ali dan Budi menggunakan update dengan fungsi + beserta angka tambahannya, sedangkan poin Citra menggunakan inc. Ketiga panggilan swap! berjalan satu per satu pada atom yang sama, dan setiap kali nilai map terbaru menjadi dasar penghitungan berikutnya. Hasil akhirnya selalu benar meskipun panggilan-panggilan itu berasal dari thread yang berbeda.

Semantik compare-and-set#

Fungsi compare-and-set! adalah operasi yang menjadi dasar swap!. Bentuknya (compare-and-set! atom nilai-lama nilai-baru), dan keluarannya true hanya bila nilai atom saat itu memang sama dengan nilai lama yang diusulkan. Apabila nilainya sudah diganti pihak lain, operasi gagal dengan nilai false tanpa mengubah apa pun. Contoh berikut memperlihatkan kedua keadaan operasi tersebut.

clojure Contoh: semantik compare-and-set!
(def penghitung (atom 0))

(println "Ganti 0 menjadi 1:" (compare-and-set! penghitung 0 1))
(println "Ganti 0 menjadi 5:" (compare-and-set! penghitung 0 5))
(println "Nilai sekarang:" @penghitung)
text Keluaran
Ganti 0 menjadi 1: true
Ganti 0 menjadi 5: false
Nilai sekarang: 1

Fungsi swap! menambahkan perulangan di atas operasi ini, yaitu membaca nilai, memanggil fungsi pengubah, lalu menjalankan compare-and-set!, dan mengulangi semuanya bila penggantian gagal. Skema inilah yang membuat atom aman digunakan dari banyak thread tanpa kunci, sekaligus alasan fungsi pengubahnya harus murni. Bila pengulangan terjadi, nilai yang dihitung fungsi dibuang dan perhitungan diulang dari nilai terbaru, sehingga tidak ada penggantian yang menimpa perubahan pihak lain.

Validator pada atom#

Atom menerima validator pada saat dibuat melalui opsi :validator. Validator berupa fungsi satu argumen yang menerima nilai usulan dan mengembalikan nilai kebenaran, dan setiap usulan nilai baru, baik lewat swap! maupun reset!, diperiksa terlebih dahulu. Apabila validator mengembalikan false, penolakan berupa IllegalStateException dengan pesan Invalid reference state, dan nilai lama tetap dipertahankan.

clojure Contoh: validator menolak nilai tidak valid
(def suhu (atom 20 :validator number?))

(println "Suhu awal:" @suhu)
(try
  (reset! suhu "dingin")
  (catch IllegalStateException e
    (println "Penolakan:" (.getMessage e))))
(println "Suhu tetap:" @suhu)
text Keluaran
Suhu awal: 20
Penolakan: Invalid reference state
Suhu tetap: 20

Nilai string “dingin” ditolak karena bukan number, sehingga suhu tetap 20. Penolakan dilempar sebagai exception alih-alih dikembalikan sebagai nilai false agar kesalahan pemanggil tidak berlalu tanpa disadari. Validator yang tepat memastikan nilai tidak sah tidak pernah terpasang, bahkan ketika nilai usulannya berasal dari perhitungan yang salah.

Kesalahan umum#

Menaruh efek samping di dalam fungsi swap!. Pencetakan ke layar atau penulisan berkas di dalam fungsi pengubah dapat terjadi berulang kali ketika thread bersaingan, karena swap! mengulang pemanggilannya. Perbaikannya dengan menjaga fungsi pengubah tetap murni dan memindahkan efek samping ke luar, setelah nilai atom selesai diganti.

Memperlakukan atom sebagai nilainya. Objek atom bukan nilai yang ia simpan, sehingga operasi aritmetika langsung atas atom menghasilkan error. Perbaikannya dengan membaca nilainya terlebih dahulu menggunakan @ atau deref, lalu mengoperasikan nilai tersebut.

Mengganti nilai dengan reset! dari perhitungan yang bergantung nilai lama. Penimpaan tanpa syarat menghapus pembaruan yang dilakukan thread lain di antara pembacaan dan penimpaan. Perbaikannya dengan menggunakan swap! yang membaca nilai terbaru secara otomatis, atau menggunakan compare-and-set! dengan pemeriksaan yang disengaja bila penimpaan memang dimaksudkan.

Ringkasan#

  • Atom adalah identity independen yang nilainya dibaca dengan deref atau @ dan diganti secara sinkron.
  • swap! menerapkan fungsi murni pada nilai saat ini melalui compare-and-set! dan mengulang prosesnya bila bersaingan, sehingga fungsi pengubahnya wajib bebas efek samping.
  • reset! menetapkan nilai baru tanpa melihat nilai lama dan digunakan hanya bila nilai lama tidak relevan.
  • Opsi :validator memeriksa setiap usulan nilai dan menolaknya dengan IllegalStateException bila tidak sah.

Referensi#