Paylaşılan veriyi korumak
Bir sync.Mutex, Lock ile Unlock arasındaki koda aynı anda yalnızca bir goroutine'in girmesine izin verir. Mutex'i koruduğu verinin yanına, genellikle aynı struct'a koyun:
Bu her zaman hits: 10000 yazdırır. Kilit olmadan aynı map'e yazan 100 goroutine programı genellikle fatal error: concurrent map writes ile çökertirdi. Bu hata çalışma zamanındaki elden geldiğince yapılan bir kontrolden gelir, recover edilemez ve çökmemesi kodun doğru olduğunu kanıtlamaz.
Önemli ayrıntılar:
sync.Mutex'in sıfır değeri kilitli değildir ve hazırdır. Kurucu yoktur.- Metotlar pointer alıcı (
*Counter) kullanır. Değer alıcı mutex'in bir kopyasını kilitlerdi ve bu hiçbir şeyi korumaz. Lock'tan hemen sonradefer c.mu.Unlock(), her dönüş yolunun ve bir panic'in kilidi bırakması anlamına gelir.- Okumalar dahil her erişim kilitten geçer. Başka bir goroutine yazarken kilitsiz bir okuma yine de bir data race'tir.
Kritik bölümü küçük tutun
defer fonksiyonun sonunda kilidi açar. Bu, yukarıdakiler gibi kısa metotlar için doğrudur. Daha uzun bir fonksiyonda paylaşılan veriye artık dokunulmadığı anda kilidi açın, böylece diğer goroutine'ler kilide ihtiyaç duymayan işi beklemez:
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
}
Ağ çağrıları, disk G/Ç'si ya da bir channel gönderimi boyunca kilit tutmak yavaş bir eşzamanlı programın en yaygın nedenidir; kilit altında bir channel gönderimi ise deadlock'un yaygın bir nedenidir.
Okuma ağırlıklı veri için RWMutex
sync.RWMutex'in iki modu vardır. RLock/RUnlock, birçok goroutine'in aynı anda tutabileceği paylaşılan bir okuma kilidi alır. Lock/Unlock ise tüm okuyucular çıkana kadar bekleyen özel yazma kilidini alır.
RWMutex, okumalar baskın olduğunda ve her okuma kilit altında gerçek iş yaptığında karşılığını verir. Tek bir map araması gibi çok küçük kritik bölümler için düz bir Mutex çoğu zaman aynı hızdadır, çünkü okuma kilidinin kendi muhasebesi vardır. Seçmeden önce benchmark yapın.
Bir okuma kilidini yazma kilidine yükseltemezsiniz. Aynı goroutine'de RLock tutarken Lock çağırmak deadlock'a girer. Önce okuma kilidini bırakın, sonra yazma kilidini alın ve koşulu yeniden kontrol edin, çünkü arada başka bir yazar veriyi değiştirmiş olabilir.
Tek değerler için sync/atomic
Tek bir sayaç ya da bayrak için sync/atomic bir mutex'ten daha basit ve ucuzdur. Kullanılacak olanlar tipli sarmalayıcılardır (Go 1.19):
Atomik işlemler aynı anda tek bir değeri korur. İki değer birlikte değişmek zorunda kaldığı anda (bir bakiye ve bir işlem sayısı, bir map ve boyutu) bir mutex kullanın. İki ayrı atomik işlemin arasına başka goroutine'ler girebilir.
sync.Once
sync.Once, aynı anda kaç goroutine çağırırsa çağırsın bir fonksiyonu tam olarak bir kez çalıştırır. Do'yu çağıran herkes ilk çağrı bitene kadar bekler. Bir şeyi tembel olarak başlatmanın standart yoludur:
Her "runs once" satırı tam olarak bir kez görünür. Do'ya aktarılan fonksiyon panic olursa Once onu yine de tamamlanmış sayar ve asla yeniden denemez. sync.OnceValues aynısını iki değer döndüren fonksiyonlar için yapar; genellikle bir değer ve bir hata.
Mutex, channel ya da sync.Map
| Durum | Kullanın |
|---|---|
| Birkaç goroutine'in yerinde güncellediği bir struct ya da map | struct içinde sync.Mutex |
| Çoğunlukla okuma, ara sıra yazma, okumalar gerçek iş yapıyor | sync.RWMutex |
| Tek bir sayaç ya da bayrak | sync/atomic |
| Tek seferlik başlatma | sync.Once, sync.OnceValue |
| Veriyi bir goroutine'den diğerine teslim etmek | bir channel |
| Anahtarları bir kez yazılıp çok kez okunan bir önbellek ya da ayrık anahtarlara dokunan goroutine'ler | sync.Map |
sync.Map, kilitli bir map'in genel bir yerine geçmez. Tip parametreleri yoktur, bu yüzden değerler any olarak geri gelir ve yalnızca tablodaki iki durumda daha hızlıdır. Bir mutex ve normal bir map ile başlayın.
Sık yapılan hatalar
- Bir mutex'i kopyalamak.
sync.Mutexiçeren bir struct'ı değerle aktarmak ya da bir değer alıcı kullanmak kilidi kopyalar.go vet,passes lock by valueya dacopies lock valueraporlar. - Tek bir goroutine'de iki kez kilitlemek. Go mutex'leri yeniden girilebilir değildir.
Inc,Get'i çağırıyorsa ve ikisi de kilidi alıyorsaIncsonsuza kadar bloklanır. Genel metotlar kilitlesin, özel yardımcılar kilidin tutulduğunu varsaysın. - Erken bir dönüşte kilidi açmayı unutmak. Bir nedeniniz yoksa
deferkullanın. - Farklı sıralarda kilitlemek. Bir goroutine önce A'yı sonra B'yi, bir başkası önce B'yi sonra A'yı kilitlerse ikisi sonsuza kadar birbirini bekleyebilir. Birden fazla kilidi her zaman aynı sırayla alın.
- Korunan veriyi dışa açmak. İç map'i bir metottan döndürmek, çağıranların onu kilit olmadan okuyup yazmasına izin verir. Bir kopya (
maps.Clone, Go 1.21) ya da tek bir değer döndürün. - Yalnızca yazmaları korumak. Kilitli yazmalarla eşzamanlı kilitsiz okumalar yine race'tir. Testlerinizi
go test -raceile çalıştırın.
Sıkça Sorulan Sorular
Go'da mutex nedir?
Bir sync.Mutex, mu.Lock() ile mu.Unlock() arasındaki kodu aynı anda yalnızca bir goroutine'in çalıştırmasına izin veren bir kilittir. Onu bir map ya da bir struct gibi birkaç goroutine'in okuyup yazdığı veriyi korumak için kullanırsınız. Sıfır değeri kilitli olmayan, kullanıma hazır bir mutex'tir.
Mutex yerine ne zaman RWMutex kullanmalıyım?
Okumalar yazmalardan çok daha fazla olduğunda ve her okuma kilidi anlamlı bir süre tuttuğunda. RLock aynı anda istediğiniz sayıda okuyucuyu içeri alır, Lock ise özel erişim için bekler. Kısa kritik bölümler için düz bir Mutex çoğu zaman aynı hızda ya da daha hızlıdır, bu yüzden geçmeden önce ölçün.
Go map'i eşzamanlı kullanım için güvenli mi?
Hayır. Eşzamanlı okumalar sorun değildir, ama başka herhangi bir okuma ya da yazmayla eşzamanlı bir yazma bir data race'tir ve çalışma zamanı genellikle bunu tespit edip fatal error: concurrent map writes (ya da concurrent map read and map write) ile çöker. Map'i bir sync.Mutex ya da sync.RWMutex ile koruyun veya belirli kullanım durumları için sync.Map kullanın.
Go'da sync.Mutex yeniden girilebilir (reentrant) mi?
Hayır. Kilidi tutan bir goroutine Lock'u yeniden çağırırsa kendini bekleyerek sonsuza kadar bloklanır. Kodu, dışa açık metotların kilidi alıp kilidin zaten tutulduğunu varsayan dışa kapalı yardımcıları çağıracağı şekilde yapılandırın.