Melindungi data bersama
sync.Mutex mengizinkan satu goroutine pada satu waktu masuk ke kode di antara Lock dan Unlock. Taruh mutex di sebelah data yang dijaganya, biasanya di struct yang sama:
Program ini selalu mencetak hits: 10000. Tanpa lock, 100 goroutine yang menulis ke map yang sama biasanya akan membuat program crash dengan fatal error: concurrent map writes. Error itu berasal dari pemeriksaan best-effort di runtime, tidak bisa di-recover, dan tidak adanya crash tidak membuktikan bahwa kodenya benar.
Detail yang penting:
- Zero value
sync.Mutexsudah tidak terkunci dan siap dipakai. Tidak perlu constructor. - Method memakai pointer receiver (
*Counter). Value receiver akan mengunci salinan mutex, yang tidak melindungi apa pun. defer c.mu.Unlock()tepat setelahLockberarti setiap jalur return, dan juga panic, akan melepas lock.- Setiap akses melewati lock, termasuk pembacaan. Pembacaan tanpa lock saat goroutine lain menulis tetap merupakan data race.
Jaga critical section tetap kecil
defer melepas lock di akhir fungsi. Itu tepat untuk method pendek seperti di atas. Di fungsi yang lebih panjang, lepas lock begitu data bersama tidak lagi disentuh, supaya goroutine lain tidak menunggu pekerjaan yang tidak butuh lock:
func (s *Store) Save(key string) error {
s.mu.Lock()
data := s.items[key] // copy what you need
s.mu.Unlock()
return writeToDisk(key, data) // slow I/O, outside the lock
}
Menahan lock selama pemanggilan jaringan, I/O disk, atau pengiriman ke channel adalah penyebab paling umum program concurrent yang lambat, dan pengiriman ke channel saat memegang lock adalah penyebab umum deadlock.
RWMutex untuk data yang sering dibaca
sync.RWMutex punya dua mode. RLock/RUnlock mengambil read lock bersama yang bisa dipegang banyak goroutine sekaligus. Lock/Unlock mengambil write lock eksklusif, yang menunggu sampai semua pembaca keluar.
RWMutex sepadan saat pembacaan mendominasi dan setiap pembacaan melakukan pekerjaan sungguhan di bawah lock. Untuk critical section yang sangat kecil seperti satu kali pencarian di map, Mutex biasa sering sama cepatnya, karena read lock punya pencatatannya sendiri. Lakukan benchmark sebelum memilih.
Kamu tidak bisa menaikkan read lock menjadi write lock. Memanggil Lock saat masih memegang RLock di goroutine yang sama akan deadlock. Lepas read lock dulu, lalu ambil write lock dan periksa kondisinya lagi, karena penulis lain mungkin sudah mengubah data di antaranya.
sync/atomic untuk satu nilai
Untuk satu counter atau flag, sync/atomic lebih sederhana dan lebih murah daripada mutex. Wrapper bertipe (Go 1.19) adalah yang sebaiknya dipakai:
Atomic melindungi satu nilai pada satu waktu. Begitu dua nilai harus berubah bersamaan (saldo dan jumlah transaksi, map dan ukurannya), pakai mutex. Dua operasi atomic yang terpisah bisa diselingi goroutine lain di antaranya.
sync.Once
sync.Once menjalankan sebuah fungsi tepat satu kali, sebanyak apa pun goroutine yang memanggilnya pada saat bersamaan. Semua yang memanggil Do menunggu sampai pemanggilan pertama selesai. Ini cara standar untuk menginisialisasi sesuatu secara lazy:
Setiap baris "runs once" muncul tepat satu kali. Kalau fungsi yang diberikan ke Do memicu panic, Once tetap menganggapnya selesai dan tidak pernah mencoba lagi. sync.OnceValues melakukan hal yang sama untuk fungsi yang mengembalikan dua nilai, biasanya sebuah nilai dan error.
Mutex, channel, atau sync.Map
| Situasi | Pakai |
|---|---|
| Struct atau map yang diperbarui langsung oleh beberapa goroutine | sync.Mutex di dalam struct |
| Kebanyakan pembacaan, sesekali penulisan, pembacaannya melakukan pekerjaan sungguhan | sync.RWMutex |
| Satu counter atau flag | sync/atomic |
| Inisialisasi satu kali | sync.Once, sync.OnceValue |
| Menyerahkan data dari satu goroutine ke goroutine lain | channel |
| Cache yang key-nya ditulis sekali dan dibaca berkali-kali, atau goroutine yang menyentuh key yang terpisah | sync.Map |
sync.Map bukan pengganti umum untuk map yang dikunci. Tipe ini tidak punya type parameter, jadi nilainya kembali sebagai any, dan hanya lebih cepat di dua kasus dalam tabel. Mulailah dengan mutex dan map biasa.
Kesalahan umum
- Menyalin mutex. Mengirim struct yang berisi
sync.Mutexsebagai nilai, atau memakai value receiver, akan menyalin lock-nya.go vetmelaporkanpasses lock by valueataucopies lock value. - Mengunci dua kali dalam satu goroutine. Mutex di Go tidak reentrant. Kalau
IncmemanggilGetdan keduanya mengambil lock,Incterblokir selamanya. Biarkan method publik yang mengunci, dan helper privat menganggap lock sudah dipegang. - Lupa melepas lock saat return lebih awal. Pakai
defer, kecuali kamu punya alasan untuk tidak memakainya. - Mengunci dengan urutan berbeda. Kalau satu goroutine mengambil lock A lalu B sementara goroutine lain mengambil B lalu A, keduanya bisa saling menunggu selamanya. Selalu ambil beberapa lock dengan urutan yang sama.
- Membuka akses ke data yang dijaga. Mengembalikan map internal dari sebuah method memungkinkan pemanggil membaca dan menulisnya tanpa lock. Kembalikan salinannya (
maps.Clone, Go 1.21) atau satu nilai saja. - Hanya melindungi penulisan. Pembacaan tanpa lock yang bersamaan dengan penulisan yang dikunci tetap merupakan race. Jalankan test-mu dengan
go test -race.
Pertanyaan yang sering diajukan
Apa itu mutex di Go?
sync.Mutex adalah kunci (lock) yang hanya mengizinkan satu goroutine pada satu waktu untuk menjalankan kode di antara mu.Lock() dan mu.Unlock(). Kamu memakainya untuk melindungi data yang dibaca dan ditulis oleh beberapa goroutine, seperti map atau struct. Zero value-nya adalah mutex yang tidak terkunci dan siap dipakai.
Kapan sebaiknya memakai RWMutex alih-alih Mutex?
Saat jumlah pembacaan jauh melebihi penulisan dan setiap pembacaan menahan lock dalam waktu yang cukup lama. RLock mengizinkan pembaca sebanyak apa pun masuk sekaligus, sedangkan Lock menunggu akses eksklusif. Untuk critical section yang pendek, Mutex biasa sering sama cepat atau lebih cepat, jadi ukur dulu sebelum beralih.
Apakah map Go aman dipakai secara concurrent?
Tidak. Pembacaan concurrent tidak masalah, tapi penulisan yang bersamaan dengan pembacaan atau penulisan lain adalah data race, dan runtime biasanya mendeteksinya lalu crash dengan fatal error: concurrent map writes (atau concurrent map read and map write). Lindungi map dengan sync.Mutex atau sync.RWMutex, atau pakai sync.Map untuk kasus-kasus khususnya.
Apakah sync.Mutex di Go reentrant?
Tidak. Kalau goroutine yang sedang memegang lock memanggil Lock lagi, ia akan terblokir selamanya menunggu dirinya sendiri. Susun kodenya sehingga method yang di-export mengambil lock lalu memanggil helper yang tidak di-export yang menganggap lock sudah dipegang.