Banyak program perlu menyiapkan sumber daya yang mahal tepat satu kali: membuka koneksi database, memuat berkas konfigurasi, atau mengisi cache global. Masalah muncul ketika puluhan goroutine membutuhkan sumber daya yang sama pada waktu yang hampir bersamaan. Pemeriksaan flag sederhana tidak lagi cukup karena dua goroutine dapat melewati pemeriksaan tersebut secara bersamaan, sehingga inisialisasi berjalan dua kali. Tipe sync.Once dari package sync menyelesaikan masalah ini dengan satu jaminan tegas: fungsi yang diberikan hanya dieksekusi tepat satu kali, sekalipun Do dipanggil dari ribuan goroutine.
Masalah Inisialisasi Ganda #
Cara paling naif untuk memuat sesuatu sekali adalah memeriksa sebuah flag sebelum menjalankan inisialisasi:
if !sudahSiap {
muatKonfigurasi() // dua goroutine bisa masuk blok ini bersamaan
sudahSiap = true
}Urutan langkahnya adalah: baca sudahSiap, jalankan muatKonfigurasi(), lalu tulis sudahSiap = true. Ketiga langkah tersebut bukan satu operasi tunggal. Dua goroutine dapat membaca sudahSiap bernilai false pada saat yang sama, keduanya lolos pemeriksaan, dan muatKonfigurasi() pun berjalan dua kali — dua pool koneksi dan dua kali biaya.
Cara Kerja sync.Once #
Tipe sync.Once adalah struct kecil dengan satu method utama, Do(f func()). Panggilan Do pertama menjalankan f, sementara pemanggil Do lain memblokir sampai f selesai. Setelah f selesai, semua panggilan Do berikutnya langsung kembali tanpa menjalankan apa pun. Dokumentasi package mencatat garansinya: kembalinya f synchronizes before kembalinya setiap panggilan once.Do(f) dalam artian Go memory model. Akibatnya, semua goroutine yang memanggil Do melihat hasil inisialisasi yang sudah selesai tanpa mutex tambahan.
Zero value dari sync.Once sudah siap dipakai; tidak ada konstruktor. Satu Once hanya berguna untuk satu fungsi dan seumur proses: setelah f selesai, tidak ada cara memakai Once yang sama untuk menjalankan fungsi lain.
Membuktikan Eksekusi Tunggal #
Berkas once-demo.go memanggil Do dari delapan goroutine sekaligus. Dua penghitung atomic.Int32 mencatat berapa kali Do dipanggil dan berapa kali fungsi inisialisasi benar-benar dieksekusi, sehingga jaminan sync.Once terbukti dengan angka, bukan sekadar klaim:
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
bukaSesi sync.Once
eksekusiInit atomic.Int32
pemanggilanDo atomic.Int32
koneksi string
)
func buatKoneksi() {
eksekusiInit.Add(1)
koneksi = "koneksi-aktif"
}
func main() {
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
pemanggilanDo.Add(1)
bukaSesi.Do(buatKoneksi)
}()
}
wg.Wait()
fmt.Println("pemanggilan Do:", pemanggilanDo.Load(), "kali")
fmt.Println("eksekusi buatKoneksi:", eksekusiInit.Load(), "kali")
fmt.Println("koneksi:", koneksi)
}Jalankan program tersebut dari direktori chapter:
go run once-demo.goOutput yang Dihasilkan:
pemanggilan Do: 8 kali
eksekusi buatKoneksi: 1 kali
koneksi: koneksi-aktifMari kita telaah baris demi baris program di atas:
- Baris 9–14 (blok
var):bukaSesibertipesync.Oncesiap pakai dari zero value-nya,eksekusiInitdanpemanggilanDoadalah penghitung atomik, dankoneksimenyimpan hasil inisialisasi. - Baris 16–19 (
buatKoneksi): fungsi inisialisasi yang mensimulasikan pekerjaan mahal; fungsi ini menambah penghitung eksekusi lalu mengisikoneksi. - Baris 24 (
for range 8): membuat delapan goroutine yang semuanya membutuhkan koneksi pada saat yang hampir bersamaan. - Baris 28 (
pemanggilanDo.Add(1)): setiap goroutine menambah penghitung pemanggilan sebelum memanggilDo, sehingga total pemanggilan tercatat dengan akurat. - Baris 29 (
bukaSesi.Do(buatKoneksi)): hanya goroutine pertama yang sampai ke baris ini menjalankanbuatKoneksi; goroutine lain menunggu sejenak lalu langsung kembali. - Baris 32 (
wg.Wait()): memastikan semua goroutine selesai sebelummainmembaca penghitung, sehingga pembacaan tidak berlomba dengan penulisan. - Baris 34–36: mencetak bukti angka: delapan pemanggilan
Do, satu eksekusibuatKoneksi, dankoneksiyang terisi.
Perhatikan bahwa koneksi ditulis oleh satu goroutine di dalam Do lalu dibaca main setelah wg.Wait(). Garansi synchronizes before dari Once dan penghalang WaitGroup membuat urutan tersebut aman tanpa mutex tambahan.
Pembantu Modern: OnceFunc, OnceValue, OnceValues #
Sejak Go 1.21, package sync menyediakan tiga fungsi pembantu yang membungkus pola yang sama dengan hasil yang tersimpan otomatis:
var ambilToken = sync.OnceValue(func() string {
return "token-sesi"
})
// pemanggilan pertama menjalankan fungsi dan menyimpan hasilnya;
// pemanggilan berikutnya mengembalikan hasil cache tanpa eksekusi ulang.
token := ambilToken()sync.OnceFunc untuk fungsi tanpa nilai balik, sync.OnceValue untuk satu nilai, dan sync.OnceValues untuk pasangan nilai seperti (data, error). Ketiganya cocok saat kita hanya butuh nilai yang dihitung sekali, tanpa mengelola struct Once secara manual.
Kapan sync.Once Dipakai #
Pola ini paling tepat untuk inisialisasi yang boleh ditunda sampai pemakaian pertama: membuka koneksi di awal hidup proses, memuat konfigurasi besar saat pertama dibutuhkan, atau mendaftarkan sesuatu ke registry global. Jika kebutuhannya menghitung ulang secara berkala atau mereset status, gunakan mutex dengan flag sendiri.
Dengan sync.Once, pembagian kerja inisialisasi antar goroutine sudah beres. Untuk kebutuhan yang lebih kecil lagi — satu angka yang perlu ditambah tanpa mutex — chapter berikutnya memperkenalkan operasi atomik dari package sync/atomic.
Referensi #
- sync.Once — dokumentasi package sync
— kontrak
Dodan garansi Go memory model. - The Go Memory Model — dasar teori urutan kejadian antar goroutine.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
once-demo.go
Unduh berkaspackage main
import (
"fmt"
"sync"
"sync/atomic"
)
var (
bukaSesi sync.Once
eksekusiInit atomic.Int32
pemanggilanDo atomic.Int32
koneksi string
)
func buatKoneksi() {
eksekusiInit.Add(1)
koneksi = "koneksi-aktif"
}
func main() {
var wg sync.WaitGroup
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
pemanggilanDo.Add(1)
bukaSesi.Do(buatKoneksi)
}()
}
wg.Wait()
fmt.Println("pemanggilan Do:", pemanggilanDo.Load(), "kali")
fmt.Println("eksekusi buatKoneksi:", eksekusiInit.Load(), "kali")
fmt.Println("koneksi:", koneksi)
}