Inisialisasi yang mahal sebaiknya berjalan tepat satu kali, sedangkan penghitung sederhana tidak selalu membutuhkan kunci. Topik ini membahas dua alat untuk kedua kebutuhan itu, yaitu sync.Once yang menjalankan satu fungsi hanya pada pemanggilan pertama dan paket sync/atomic yang menyediakan operasi atomik, yakni operasi yang berjalan sebagai satu langkah tak terputus. Setelah membacanya, pemilihan antara kunci, Once, dan operasi atomik dapat ditentukan sesuai bentuk state yang dilindungi, lengkap dengan batasan masing-masing.
sync.Once dan inisialisasi satu kali#
Konfigurasi yang dibaca dari berkas atau sumber jaringan lazimnya dimuat sekali dan dipakai bersama. Tantangannya muncul ketika beberapa goroutine mencapai titik pemuatan itu hampir bersamaan, karena tanpa alat khusus fungsi pemuatan dapat berjalan lebih dari satu kali. sync.Once menyelesaikan persoalan tersebut melalui metode Do yang menerima satu fungsi dan menjamin fungsi itu berjalan tepat satu kali sepanjang masa hidup objek Once. Pemanggil lain menunggu hingga fungsi pertama selesai, lalu seluruh pemanggil melihat hasil yang sama. Program berikut meniru pemuatan pengaturan yang dipanggil empat goroutine sekaligus, dan penghitungnya membuktikan jumlah eksekusi yang terjadi.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
pengaturan map[string]string
sekali sync.Once
jumlahMuat atomic.Int32
)
// muatPengaturan meniru pembacaan konfigurasi yang mahal dan hanya boleh
// berjalan satu kali meskipun dipanggil dari banyak goroutine.
func muatPengaturan() map[string]string {
sekali.Do(func() {
jumlahMuat.Add(1)
pengaturan = map[string]string{
"bahasa": "id",
"tema": "gelap",
}
})
return pengaturan
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
muatPengaturan()
}()
}
wg.Wait()
fmt.Println("jumlah pemuatan:", jumlahMuat.Load())
fmt.Println("bahasa:", muatPengaturan()["bahasa"])
}jumlah pemuatan: 1
bahasa: idPenghitung menunjukkan angka 1 pada setiap eksekusi, meskipun empat goroutine memanggil muatPengaturan hampir bersamaan. Fungsi yang diberikan kepada Do hanya berjalan pada pemanggil pertama, sedangkan tiga pemanggil lain menunggu lalu menerima peta yang sudah terisi. Sejak Go 1.21 terdapat pula sync.OnceFunc, sync.OnceValue, dan sync.OnceValues yang membungkus pola ini menjadi satu fungsi siap pakai, sehingga variabel Once tidak perlu ditulis manual untuk kasus yang sederhana.
Zero value dari sync.Once juga sudah siap dipakai, sehingga variabelnya cukup dideklarasikan tanpa konstruktor. Metode Do hanya kembali setelah fungsi yang diberikan selesai dijalankan, sehingga pemanggil yang datang belakangan sudah melihat hasilnya utuh. Penempatan variabel Once perlu dijaga agar berumur panjang, misalnya sebagai variabel tingkat paket atau field struct yang hidup selama aplikasi berjalan, karena Once yang dibuat ulang akan menjalankan fungsinya sekali lagi.
mkdir -p /tmp/go-once-atomic
cp muat-sekali.go penghitung-atomic.go /tmp/go-once-atomic/
cd /tmp/go-once-atomic
go run muat-sekali.gojumlah pemuatan: 1
bahasa: idOperasi atomik tanpa kunci#
Paket sync/atomic menyediakan operasi yang berjalan sebagai satu langkah tak terputus, sehingga goroutine lain tidak pernah melihat keadaan setengah selesai. Untuk satu bilangan bulat yang hanya ditambah dan dibaca, operasi ini lebih murah daripada kunci karena tidak ada antrean goroutine yang harus dikelola. Tipe seperti atomic.Int64 menyediakan metode Add, Load, Store, Swap, dan CompareAndSwap sesuai kebutuhan. Program berikut menghitung pengunjung perpustakaan yang sama tanpa satu pun kunci.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var (
pengunjung atomic.Int64
wg sync.WaitGroup
)
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 100; n++ {
pengunjung.Add(1)
}
}()
}
wg.Wait()
fmt.Println("total pengunjung tercatat:", pengunjung.Load())
}total pengunjung tercatat: 400Nilai akhirnya kembali selalu 400 karena Add berjalan sebagai satu langkah yang tidak dapat diselipkan. Pembanding yang menarik adalah versi tanpa sinkronisasi pada potongan berikut, yang keluarannya sengaja tidak ditampilkan karena memang berubah-ubah antareksekusi.
// tidak-dijalankan
var (
pengunjung int64
wg sync.WaitGroup
)
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for n := 0; n < 100; n++ {
pengunjung++ // race condition: baca, tambah, dan tulis tanpa kunci
}
}()
}
wg.Wait()
fmt.Println(pengunjung) // bisa mencetak 400, bisa juga angka di bawahnyaOperasi pengunjung++ tetap tersusun atas tiga langkah, sehingga dua goroutine dapat membaca nilai yang sama lalu menulis hasil yang sama dan satu penambahan pun hilang. Keluarannya berpindah-pindah antareksekusi, dan go run -race melaporkan data race pada baris tersebut. Program sebelumnya menghindari hal ini karena Add menggabungkan tiga langkah itu menjadi satu operasi atomik. Sebelum Go 1.19, operasi atomik pada field struct di arsitektur 32-bit menuntut pengaturan perataan memori secara manual, sedangkan tipe atomic.Int64 beserta saudaranya mengurus perataan itu sendiri pada semua arsitektur sehingga cukup ditulis langsung.
Memilih antara kunci, Once, dan atomic#
Aturan pemilihannya bertumpu pada bentuk state yang dilindungi. Operasi atomik menjaga satu nilai pada satu waktu. Dua penghitung yang harus naik bersama tidak dapat dijaga dengan dua operasi atomik terpisah, karena pembaca dapat melihat keduanya dalam keadaan tak sepadan. Kombinasi semacam itulah yang menuntut kunci. Sebaliknya, memasang Mutex untuk penghitung tunggal bukanlah kesalahan, hanya saja lebih berat daripada yang diperlukan. Sebuah Once juga tidak menggantikan kunci karena tugasnya hanya pada pemanggilan pertama, sedangkan akses sesudahnya tetap memerlukan perlindungan.
| Kebutuhan | Alat yang tepat |
|---|---|
| Fungsi inisialisasi yang berjalan satu kali | sync.Once |
| Satu bilangan bulat atau pointer yang ditambah dan dibaca | sync/atomic |
| Beberapa field yang harus berubah bersamaan | sync.Mutex |
| Banyak pembaca dengan sedikit penulis | sync.RWMutex |
Alat-alat ini juga dapat bekerja berdampingan pada program yang sama, misalnya Once untuk pemuatan awal, atomic untuk penghitung operasi, dan Mutex untuk cache yang berubah dari waktu ke waktu. Pembagian peran seperti itu justru lebih mudah dibaca daripada satu kunci besar yang mengawasi seluruh state. Penilaian akhirnya tetap kembali ke bentuk state dan pola aksesnya, bukan ke kebiasaan menyalin kode lama.
Kesalahan umum#
- Mengandalkan operasi atomik untuk beberapa field yang harus konsisten bersama. Nilai-nilai itu tetap dapat terbaca dalam keadaan tak cocok meskipun masing-masing benar. Perbaikannya, kunci seluruh kelompok field dengan
Mutex. - Membaca variabel atomik dengan penugasan biasa. Pembacaan tanpa
Loadtidak dijamin melihat nilai terbaru dan detektor race melaporkannya. Perbaikannya, seluruh akses termasuk yang hanya membaca tetap melalui metode paketatomic. - Menganggap fungsi pada
Doberjalan ulang setelah gagal. Fungsi yangpanictetap dianggap selesai, sehingga pemanggilan berikutnya tidak mengulanginya. Perbaikannya, tangani kemungkinan gagal di dalam fungsi itu sendiri sebelum hasilnya diumumkan.
Ringkasan#
sync.Oncemenjalankan satu fungsi tepat satu kali dan membuat pemanggil lain menunggu hasilnya.sync/atomicmenyediakanAdd,Load,Store,Swap, danCompareAndSwapuntuk satu nilai tanpa kunci.- Operasi atomik menutup race condition pada satu nilai, sedangkan kunci tetap diperlukan untuk beberapa field yang harus konsisten bersama.
sync.OnceFuncbeserta saudaranya membungkus polaOncemenjadi fungsi siap pakai sejak Go 1.21.
Referensi#
- sync — Go Documentation
— Dokumentasi resmi
sync.Oncebeserta perilaku fungsinya. - sync/atomic — Go Documentation — Daftar operasi atomik dan aturan pemakaiannya.
- The Go Memory Model — The Go Project — Jaminan urutan yang menyertai operasi atomik.