Eşzamanlılık
Go dili eşzamanlılık için doğal destek sunar. Bu dilin çekirdeğidir ve kullanım kolaylığı nispeten düşüktür. Geliştiriciler altta yatan uygulamayı çok fazla düşünmeden oldukça iyi bir eşzamanlı uygulama oluşturabilir ve geliştiricilerin alt sınırını yükseltir.
Goroutine
Goroutine, hafif bir iş parçacığıdır veya kullanıcı modu iş parçacığı olarak adlandırılır. İşletim sistemi tarafından doğrudan zamanlanmaz, Go dilinin kendi zamanlayıcısı tarafından çalışma zamanında zamanlanır. Bu nedenle bağlam değiştirme maliyeti çok düşüktür ve bu da Go'nun eşzamanlılık performansının iyi olmasının nedenlerinden biridir. Goroutine kavramı ilk olarak Go tarafından ortaya atılmamıştır ve Go goroutine'i destekleyen ilk dil de değildir. Ancak Go, goroutine ve eşzamanlılığı oldukça basit ve zarif bir şekilde destekleyebilen ilk dildir.
Go'da bir goroutine oluşturmak çok basittir. Sadece go anahtar kelimesi ile hızlı bir şekilde bir goroutine başlatabilirsiniz. go anahtar kelimesinden sonra bir fonksiyon çağrısı gelmelidir. Örnek aşağıdaki gibidir
::: ipucu
Dönüş değeri olan yerleşik fonksiyonlar go anahtar kelimesinden sonra kullanılamaz. Aşağıdaki hatalı örneğe bakın
go make([]int,10) // go discards result of make([]int, 10) (value of type []int):::
func main() {
go fmt.Println("hello world!")
go hello()
go func() {
fmt.Println("hello world!")
}()
}
func hello() {
fmt.Println("hello world!")
}Yukarıdaki üç goroutine başlatma yöntemi çalışır. Ancak bu örnek çalıştırıldığında çoğu durumda hiçbir şey çıktılanmaz. Goroutine'ler eşzamanlı olarak çalışır ve sistem goroutine oluşturmak için zamana ihtiyaç duyar. Bundan önce, ana goroutine çoktan çalışmayı bitirmiştir. Ana iş parçacığı çıktıktan sonra, diğer alt goroutine'ler de doğal olarak çıkar. Ayrıca goroutine'lerin çalışma sırası da belirsizdir ve tahmin edilemez. Aşağıdaki örneğe bakın
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
fmt.Println("end")
}Bu bir döngü içinde goroutine başlatan bir örnektir. Ne çıktılanacağını asla kesin olarak tahmin edemezsiniz. Alt goroutine'ler çalışmaya başlamadan önce ana goroutine bitmiş olabilir. Durum şu şekildedir
start
endVeya sadece bazı alt goroutine'ler ana goroutine çıkmadan önce başarıyla çalışmış olabilir. Durum şu şekildedir
start
0
1
5
3
4
6
7
endEn basit çözüm, ana goroutine'in biraz beklemesini sağlamaktır. time paketi altındaki Sleep fonksiyonunu kullanmanız gerekir. Bu, mevcut goroutine'in bir süre duraklatılmasını sağlar. Örnek aşağıdaki gibidir
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
}
// 1ms duraklat
time.Sleep(time.Millisecond)
fmt.Println("end")
}Tekrar çalıştırıldığında çıktı şu şekildedir. Tüm sayıların eksiksiz olarak çıktılanmış olduğunu görebilirsiniz
start
0
1
5
2
3
4
6
8
9
7
endAncak sıra hala karışıktır. Bu nedenle her döngünün biraz beklemesini sağlayın. Örnek aşağıdaki gibidir
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go fmt.Println(i)
time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
fmt.Println("end")
}Şimdi çıktı normal sıradadır
start
0
1
2
3
4
5
6
7
8
9
endYukarıdaki örnekte sonuç çıktısı mükemmeldir. Peki eşzamanlılık sorunu çözüldü mü? Hayır, hiç de değil. Eşzamanlı programlar için kontrol edilemeyen faktörler çok fazladır. Çalışma zamanı, sırası, işlem süresi vb. Döngüdeki alt goroutine'in işi sadece basit bir sayı çıktısı değil de çok büyük ve karmaşık bir görevse, süresi belirsizse, o zaman önceki sorunlar tekrar ortaya çıkacaktır. Aşağıdaki koda bakın
func main() {
fmt.Println("start")
for i := 0; i < 10; i++ {
go hello(i)
time.Sleep(time.Millisecond)
}
time.Sleep(time.Millisecond)
fmt.Println("end")
}
func hello(i int) {
// Rastgele gecikme simülasyonu
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println(i)
}Bu kodun çıktısı hala belirsizdir. Aşağıdaki olası durumlardan biridir
start
0
3
4
endBu nedenle time.Sleep iyi bir çözüm değildir. Neyse ki Go birçok eşzamanlılık kontrol yöntemi sağlar. Yaygın olarak kullanılan üç eşzamanlılık kontrol yöntemi vardır:
channel: KanalWaitGroup: SinyalContext: Bağlam
Üç yöntem farklı uygulama senaryolarına sahiptir. WaitGroup dinamik olarak belirli sayıda goroutine'i kontrol edebilir. Context alt goroutine iç içe geçmiş düzeylerin daha derin olduğu durumlar için daha uygundur. Kanal goroutine'ler arası iletişim için daha uygundur. Daha geleneksel kilit kontrolü için Go da destek sağlar:
Mutex: Karşılıklı dışlama kilidiRWMutex: Okuma-yazma karşılıklı dışlama kilidi
Kanal
channel, kanal olarak çevrilir. Go kanalın işlevini şu şekilde açıklar:
Do not communicate by sharing memory; instead, share memory by communicating.
Yani mesajlar aracılığıyla bellek paylaşımı yapılır. channel bunun için yaratılmıştır. Goroutine'ler arası iletişim için bir çözümdür ve aynı zamanda eşzamanlılık kontrolü için de kullanılabilir. Önce channel'ın temel sözdizimini tanıyalım. Go'da chan anahtar kelimesi kanalı temsil eder. Aynı zamanda kanalın depolama türünü de belirtmelisiniz. Depolanan verilerin ne tür olduğunu belirtmek için aşağıdaki örnek normal bir kanalın görünümüdür.
var ch chan intBu bir kanal bildirim ifadesidir. Bu noktada kanal henüz başlatılmamıştır ve değeri nil'dir. Doğrudan kullanılamaz.
Oluşturma
Kanal oluştururken bir ve yalnızca bir yöntem vardır. Bu da yerleşik make fonksiyonunu kullanmaktır. Kanal için make fonksiyonu iki parametre alır. Birincisi kanal türüdür, ikincisi opsiyonel parametredir ve kanalın arabellek boyutudur. Örnek aşağıdaki gibidir
intCh := make(chan int)
// Arabellek boyutu 1 olan kanal
strCh := make(chan string, 1)Bir kanalı kullandıktan sonra mutlaka kapatmalısınız. Kanalı kapatmak için yerleşik close fonksiyonunu kullanın. Fonksiyon imzası şu şekildedir.
func close(c chan<- Type)Kanal kapatma örneği aşağıdaki gibidir
func main() {
intCh := make(chan int)
// bir şeyler yap
close(intCh)
}Bazen kanalı kapatmak için defer kullanmak daha iyi olabilir.
Okuma-Yazma
Kanal için Go, okuma-yazma işlemlerini temsil etmek için iki çok görsel operatör kullanır:
ch <-: Kanala veri yazmayı temsil eder
<- ch: Kanaldan veri okumayı temsil eder
<- veri akış yönünü çok canlı bir şekilde gösterir. int türü bir kanal okuma-yazma örneğine bakalım
func main() {
// Arabellek yoksa deadlock oluşur
intCh := make(chan int, 1)
defer close(intCh)
// Veri yaz
intCh <- 114514
// Veri oku
fmt.Println(<-intCh)
}Yukarıdaki örnekte arabellek boyutu 1 olan int tipi bir kanal oluşturulur. 114514 verisi yazılır, ardından veri okunur ve çıktılanır. Son olarak kanal kapatılır. Okuma işlemi için ikinci bir dönüş değeri vardır. Bir boolean türü değerdir ve verinin başarıyla okunup okunmadığını gösterir
ints, ok := <-intChKanal içindeki veri akış yöntemi kuyruk ile aynıdır. Yani ilk giren ilk çıkar (FIFO). Goroutine'lerin kanal üzerindeki işlemleri senkronizedir. Belirli bir anda yalnızca bir goroutine kanala veri yazabilir ve aynı zamanda yalnızca bir goroutine kanaldan veri okuyabilir.
Arabelleksiz
Arabelleksiz kanal için arabellek kapasitesi 0 olduğundan geçici olarak hiçbir veri depolanamaz. Arabelleksiz kanal veri depolayamadığından, kanala veri yazarken hemen başka bir goroutine tarafından okunmalıdır. Aksi takdirde engellenir ve bekler. Veri okurken de aynı durum geçerlidir. Bu da aşağıdaki görünüşte normal kodun neden deadlock'a neden olduğunu açıklar.
func main() {
// Arabelleksiz kanal oluştur
ch := make(chan int)
defer close(ch)
// Veri yaz
ch <- 123
// Veri oku
n := <-ch
fmt.Println(n)
}Arabelleksiz kanal senkronize şekilde kullanılmamalıdır. Doğrusu veri göndermek için yeni bir goroutine başlatmaktır. Aşağıdaki örnek gibi
func main() {
// Arabelleksiz kanal oluştur
ch := make(chan int)
defer close(ch)
go func() {
// Veri yaz
ch <- 123
}()
// Veri oku
n := <-ch
fmt.Println(n)
}Arabellekli
Kanalın arabelleği olduğunda, tıpkı bir engelleme kuyruğu gibidir. Boş kanalı okumak ve dolu kanala yazmak engellemeye neden olur. Arabelleksiz kanal veri gönderirken hemen biri tarafından alınmalıdır. Aksi takdirde sürekli engellenir. Arabellekli kanal için bu gerekmez. Arabellekli kanala veri yazarken, veri önce arabelleğe konur. Yalnızca arabellek kapasitesi dolduğunda, kanaldan veri okumak için goroutine'i engelleme beklemesi gerekir. Aynı şekilde, arabellekli kanalı okurken, önce arabellekten veri okunur. Arabellekte veri kalmayana kadar kanala veri yazmak için goroutine'i engelleme beklemesi gerekir. Bu nedenle, arabelleksiz kanalda deadlock'a neden olan örnek burada sorunsuz çalışabilir.
func main() {
// Arabellekli kanal oluştur
ch := make(chan int, 1)
defer close(ch)
// Veri yaz
ch <- 123
// Veri oku
n := <-ch
fmt.Println(n)
}Sorunsuz çalışabilse de, bu senkron okuma-yazma yöntemi çok tehlikelidir. Kanal arabelleği boşaldığında veya dolduğunda, sonsuza kadar engellenecektir. Çünkü kanala veri yazmak veya okumak için başka goroutine yoktur. Aşağıdaki örneğe bakalım
func main() {
// Arabellekli kanal oluştur
ch := make(chan int, 5)
// İki arabelleksiz kanal oluştur
chW := make(chan struct{})
chR := make(chan struct{})
defer func() {
close(ch)
close(chW)
close(chR)
}()
// Yazmaktan sorumlu
go func() {
for i := 0; i < 10; i++ {
ch <- i
fmt.Println("Yazma", i)
}
chW <- struct{}{}
}()
// Okumaktan sorumlu
go func() {
for i := 0; i < 10; i++ {
// Her okumada 1 milisaniye harcanır
time.Sleep(time.Millisecond)
fmt.Println("Okuma", <-ch)
}
chR <- struct{}{}
}()
fmt.Println("Yazma tamamlandı", <-chW)
fmt.Println("Okuma tamamlandı", <-chR)
}Burada toplam 3 kanal oluşturuldu. Goroutine'ler arası iletişim için bir arabellekli kanal ve alt/üst goroutine'lerin çalışma sırasını senkronize etmek için iki arabelleksiz kanal. Okumadan sorumlu goroutine her okumadan önce 1 milisaniye bekler. Yazmaktan sorumlu goroutine en fazla 5 veri yazabilir. Çünkü kanal arabelleği maksimum 5'tir. Veri okuyan goroutine olmadan önce, engelleme beklemesi gerekir. Bu nedenle örnek çıktısı şu şekildedir
Yazma 0
Yazma 1
Yazma 2
Yazma 3
Yazma 4 // 5 tane yazıldı, arabellek doldu, diğer goroutine'in okumasını bekle
Okuma 0
Yazma 5 // Bir oku, bir yaz
Okuma 1
Yazma 6
Okuma 2
Yazma 7
Okuma 3
Yazma 8
Yazma 9
Okuma 4
Yazma tamamlandı {} // Tüm veriler gönderildi, yazma goroutine'i tamamlandı
Okuma 5
Okuma 6
Okuma 7
Okuma 8
Okuma 9
Okuma tamamlandı {} // Tüm veriler okundu, okuma goroutine'i tamamlandıYazmaktan sorumlu goroutine'in başlangıçta 5 veri gönderdiğini görebilirsiniz. Arabellek dolduktan sonra okuma goroutine'inin okumasını beklemeye başlar. Sonra her 1 milisaniyede bir veri okunduğunda, arabellekte yer açıldığında yazma goroutine'i bir veri yazar. Tüm veriler gönderilene kadar, yazma goroutine'i çalışmayı bitirir. Ardından okuma goroutine'i arabellekteki tüm verileri okuduktan sonra, okuma goroutine'i de çalışmayı bitirir. Son olarak ana goroutine çıkar.
::: ipucu
Yerleşik len fonksiyonu ile kanal arabelleğindeki veri sayısına erişebilirsiniz. cap ile kanal arabelleğinin boyutuna erişebilirsiniz.
func main() {
ch := make(chan int, 5)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(len(ch), cap(ch))
}Çıktı
3 5:::
Kanal engelleme koşullarını kullanarak, ana goroutine'in alt goroutine'lerin çalışması bitene kadar beklediği bir örnek kolayca yazılabilir
func main() {
// Bir arabelleksiz kanal oluştur
ch := make(chan struct{})
defer close(ch)
go func() {
fmt.Println(2)
// Yaz
ch <- struct{}{}
}()
// Engelleme bekleme okuması
<-ch
fmt.Println(1)
}Çıktı
2
1Arabellekli kanal kullanılarak basit bir karşılıklı dışlama kilidi de uygulanabilir. Aşağıdaki örneğe bakın
var count = 0
// Arabellek boyutu 1 olan kanal
var lock = make(chan struct{}, 1)
func Add() {
// Kilitle
lock <- struct{}{}
fmt.Println("Mevcut sayım", count, "toplama yapılıyor")
count += 1
// Kilidi aç
<-lock
}
func Sub() {
// Kilitle
lock <- struct{}{}
fmt.Println("Mevcut sayım", count, "çıkarma yapılıyor")
count -= 1
// Kilidi aç
<-lock
}Kanal arabelleğinin boyutu 1 olduğundan, arabellekte en fazla bir veri depolanabilir. Add ve Sub fonksiyonları her işlem yapmadan önce kanala veri göndermeye çalışır. Arabellek boyutu 1 olduğundan, eğer başka bir goroutine zaten veri yazdıysa ve arabellek dolduysa, mevcut goroutine arabellekte yer açılana kadar engelleme beklemelidir. Bu şekilde, belirli bir anda en fazla bir goroutine count değişkenini değiştirebilir. Böylece basit bir karşılıklı dışlama kilidi uygulanmış olur.
Dikkat Edilmesi Gerekenler
Aşağıda bir özet bulunmaktadır. Kanalların yanlış kullanımı aşağıdaki durumlarda kanal engellemesine neden olabilir:
Arabelleksiz kanalı okuma-yazma
Arabelleksiz kanala doğrudan senkron okuma-yazma işlemi yapıldığında mevcut goroutine engellenir
func main() {
// Arabelleksiz kanal oluşturuldu
intCh := make(chan int)
defer close(intCh)
// Veri gönder
intCh <- 1
// Veri oku
ints, ok := <-intCh
fmt.Println(ints, ok)
}Boş arabellekli kanalı okuma
Arabelleği boş olan bir kanalı okurken, mevcut goroutine engellenir
func main() {
// Arabellekli kanal oluşturuldu
intCh := make(chan int, 1)
defer close(intCh)
// Arabellek boş, diğer goroutine'lerin veri yazmasını engelleme bekliyor
ints, ok := <-intCh
fmt.Println(ints, ok)
}Dolu arabellekli kanala yazma
Kanal arabelleği dolduğunda, veri yazmak mevcut goroutine'i engeller
func main() {
// Arabellekli kanal oluşturuldu
intCh := make(chan int, 1)
defer close(intCh)
intCh <- 1
// Doldu, diğer goroutine'lerin okumasını engelleme bekliyor
intCh <- 1
}Kanal nil olduğunda
Kanal nil olduğunda, okuma veya yazma ne olursa olsun mevcut goroutine engellenir
func main() {
var intCh chan int
// Yaz
intCh <- 1
}func main() {
var intCh chan int
// Oku
fmt.Println(<-intCh)
}Kanal engelleme koşulları hakkında iyi ustalaşmanız ve aşina olmanız gerekir. Çoğu durumda bu sorunlar çok gizlidir ve örneklerdeki gibi doğrudan değildir.
Aşağıdaki durumlar ayrıca panic'e neden olur:
nil kanalı kapatma
Kanal nil olduğunda, close fonksiyonu ile kapatma işlemi panic'e neden olur
func main() {
var intCh chan int
close(intCh)
}Kapatılmış kanala yazma
Kapatılmış bir kanala veri yazmak panic'e neden olur
func main() {
intCh := make(chan int, 1)
close(intCh)
intCh <- 1
}Kapatılmış kanalı kapatma
Bazı durumlarda, kanal katman katman iletilebilir. Çağıran kimin kanalı kapatması gerektiğini bilemeyebilir. Bu durumda, zaten kapatılmış bir kanalı kapatmaya çalışmak panic'e neden olur.
func main() {
ch := make(chan int, 1)
defer close(ch)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// Sadece kanala veri gönderebilir
ch <- 1
close(ch)
}Tek Yönlü Kanal
Çift yönlü kanal hem yazılabilir hem de okunabilir demektir. Yani kanalın her iki tarafında da işlem yapılabilir. Tek yönlü kanal sadece okunabilir veya sadece yazılabilir kanal demektir. Yani sadece kanalın bir tarafında işlem yapılabilir. Manuel olarak oluşturulan sadece okunabilir veya sadece yazılabilir kanalın çok fazla anlamı yoktur. Çünkü kanalı okuyup yazamamak, varoluş amacını yitirmesine neden olur. Tek yönlü kanal genellikle kanal davranışını kısıtlamak için kullanılır. Genellikle fonksiyon parametrelerinde ve dönüş değerlerinde görünür. Örneğin kanalı kapatmak için kullanılan yerleşik close fonksiyonunun fonksiyon imzası tek yönlü kanalı kullanır.
func close(c chan<- Type)Veya yaygın olarak kullanılan time paketi altındaki After fonksiyonu
func After(d Duration) <-chan Timeclose fonksiyonunun parametresi sadece yazılabilir kanaldır. After fonksiyonunun dönüş değeri sadece okunabilir kanaldır. Bu nedenle tek yönlü kanal sözdizimi şu şekildedir:
- Ok işareti
<-önde ise, sadece okunabilir kanaldır. Örneğin<-chan int - Ok işareti
<-sonda ise, sadece yazılabilir kanaldır. Örneğinchan<- string
Sadece okunabilir kanala veri yazmaya çalışıldığında, derleme başarısız olur
func main() {
timeCh := time.After(time.Second)
timeCh <- time.Now()
}Hata şu şekildedir, anlamı çok açıktır
invalid operation: cannot send to receive-only channel timeCh (variable of type <-chan time.Time)Sadece yazılabilir kanaldan veri okumak da aynıdır.
Çift yönlü kanal tek yönlü kanala dönüştürülebilir. Tersine ise dönüştürülemez. Normalde, çift yönlü kanalı bir goroutine'e veya fonksiyona iletirken ve okumasını/göndermesini istemediğinizde, tek yönlü kanalı kullanarak diğer tarafın davranışını kısıtlayabilirsiniz.
func main() {
ch := make(chan int, 1)
go write(ch)
fmt.Println(<-ch)
}
func write(ch chan<- int) {
// Sadece kanala veri gönderebilir
ch <- 1
}Sadece okunabilir kanal da aynı mantıktadır
::: ipucu
chan referans türüdür. Go'nun fonksiyon parametreleri değer kopyalaması olsa da, referans hala aynıdır. Bu, sonraki kanal prensiplerinde açıklanacaktır.
:::
for range
for range ifadesi kullanılarak arabellekli kanaldaki veriler okunabilir. Aşağıdaki örnek gibi
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
for n := range ch {
fmt.Println(n)
}
}Genellikle for range diğer yinelemeli veri yapılarını yineleirken iki dönüş değeri vardır. Birincisi indeks, ikincisi eleman değeridir. Ancak kanal için sadece bir dönüş değeri vardır. for range kanaldaki elemanları sürekli okur. Kanal arabelleği boş olduğunda veya arabellek olmadığında, diğer goroutine'ler kanala veri yazana kadar engelleme bekler. Bu nedenle çıktı şu şekildedir:
0
1
2
3
4
5
6
7
8
9
fatal error: all goroutines are asleep - deadlock!Yukarıdaki kodun deadlock'a girdiğini görebilirsiniz. Çünkü alt goroutine çalışmayı bitirmiştir ve ana goroutine hala diğer goroutine'lerin kanala veri yazmasını engelleme beklemektedir. Bu nedenle kanal veri gönderimi bittikten sonra kapatılmalıdır. Aşağıdaki kod gibi değiştirin
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
// Kanalı kapat
close(ch)
}()
for n := range ch {
fmt.Println(n)
}
}Yazma işlemi bittikten sonra kanalı kapatın. Yukarıdaki kod artık deadlock'a girmez. Daha önce kanalı okumanın iki dönüş değeri olduğu belirtilmişti. for range kanalı yinelediğinde, veri başarıyla okunamadığında döngüden çıkar. İkinci dönüş değeri verinin başarıyla okunup okunamadığını ifade eder, kanalın kapatılıp kapatılmadığını değil. Kanal kapatılmış olsa bile, arabellekli kanal için hala veri okunabilir ve ikinci dönüş değeri hala true'dur. Aşağıdaki örneğe bakın
func main() {
ch := make(chan int, 10)
for i := 0; i < 5; i++ {
ch <- i
}
// Kanalı kapat
close(ch)
// Tekrar veri oku
for i := 0; i < 6; i++ {
n, ok := <-ch
fmt.Println(n, ok)
}
}Çıktı sonucu
0 true
1 true
2 true
3 true
4 true
0 falseKanal kapatıldığı için, arabellek boş olsa bile, veri okumak mevcut goroutine'i engellemez. Altıncı yinelemede okunan değerin sıfır değeri olduğunu ve ok'un false olduğunu görebilirsiniz.
::: ipucu
Kanal kapatma zamanlaması hakkında, kanalı veri gönderen tarafta kapatmak gerekir. Alıcı tarafta kapatmamalısınız. Çünkü çoğu durumda alıcı sadece veri alır ve kanalı ne zaman kapatması gerektiğini bilmez.
:::
WaitGroup
sync.WaitGroup, sync paketi tarafından sağlanan bir yapıdır. WaitGroup, beklemeyi yürütme anlamına gelir. Bir grup goroutine'i beklemek için kolayca kullanılabilir. Bu yapı sadece üç yöntemi dışarıya açar.
Add yöntemi beklenmesi gereken goroutine sayısını belirtmek için kullanılır
func (wg *WaitGroup) Add(delta int)Done yöntemi mevcut goroutine'in çalışmayı tamamladığını belirtir
func (wg *WaitGroup) Done()Wait yöntemi alt goroutine'lerin bitmesini bekler, aksi takdirde engeller
func (wg *WaitGroup) Wait()WaitGroup kullanımı çok basittir, kutudan çıkar çıkmaz kullanılabilir. İç uygulaması sayaç + sinyalizasyondur. Program başladığında Add çağrılarak sayaç başlatılır. Her goroutine bittiğinde Done çağrılır ve sayaç 1 azalır. 0'a düşene kadar, bu sırada ana goroutine Wait çağrısı yapar ve tüm sayaçlar 0'a düşene kadar engellenir. Sonra uyanır. Basit bir kullanım örneğine bakalım
func main() {
var wait sync.WaitGroup
// Alt goroutine sayısını belirt
wait.Add(1)
go func() {
fmt.Println(1)
// Çalışma tamamlandı
wait.Done()
}()
// Alt goroutine'i bekle
wait.Wait()
fmt.Println(2)
}Bu kod her zaman önce 1'i sonra 2'yi çıktılar. Ana goroutine alt goroutine çalışmayı bitirene kadar bekler ve sonra çıkar.
1
2Goroutine tanıtımındaki ilk örneğe göre aşağıdaki gibi değişiklik yapılabilir
func main() {
var mainWait sync.WaitGroup
var wait sync.WaitGroup
// 10 say
mainWait.Add(10)
fmt.Println("start")
for i := 0; i < 10; i++ {
// Döngü içinde 1 say
wait.Add(1)
go func() {
fmt.Println(i)
// İki sayımı 1 azalt
wait.Done()
mainWait.Done()
}()
// Mevcut döngünün goroutine'inin bitmesini bekle
wait.Wait()
}
// Tüm goroutine'lerin bitmesini bekle
mainWait.Wait()
fmt.Println("end")
}Burada time.Sleep yerine sync.WaitGroup kullanıldı. Goroutine'lerin eşzamanlı çalışma sırası daha kontrol edilebilir. Kaç kez çalıştırılırsa çalıştırılsın, çıktı şu şekildedir
start
0
1
2
3
4
5
6
7
8
9
endWaitGroup genellikle goroutine sayısının dinamik olarak ayarlanabildiği durumlarda uygundur. Örneğin goroutine sayısı önceden biliniyorsa veya çalışma sırasında dinamik olarak ayarlanması gerekiyorsa. WaitGroup değeri kopyalanmamalıdır. Kopyalanan değer kullanılmamalıdır. Özellikle fonksiyon parametresi olarak iletirken, değer yerine işaretçi iletilmelidir. Kopyalanan değer kullanılırsa, sayaç gerçek WaitGroup üzerinde çalışmaz. Bu, ana goroutine'in sürekli engellenmesine ve programın normal çalışmamasına neden olabilir. Aşağıdaki kod örneği
func main() {
var mainWait sync.WaitGroup
mainWait.Add(1)
hello(mainWait)
mainWait.Wait()
fmt.Println("end")
}
func hello(wait sync.WaitGroup) {
fmt.Println("hello")
wait.Done()
}Hata tüm goroutine'lerin çıktığını, ancak ana goroutine'in hala beklediğini gösterir. Bu deadlock oluşturur. Çünkü hello fonksiyonu içinde şekil parametresi WaitGroup üzerinde Done çağrısı yapmak, orijinal mainWait üzerinde çalışmaz. Bu nedenle işaretçi kullanılarak iletilmelidir.
hello
fatal error: all goroutines are asleep - deadlock!::: ipucu
Sayaç negatif olduğunda veya sayaç sayısı alt goroutine sayısından büyük olduğunda, panic oluşur.
:::
Context
Context, bağlam olarak çevrilir. Go tarafından sağlanan bir eşzamanlılık kontrol çözümüdür. Kanal ve WaitGroup'a kıyasla, alt ve daha derin düzeydeki goroutine'leri daha iyi kontrol edebilir. Context kendisi bir arayüzdür. Bu arayüzü uygulayan her şey bağlam olarak adlandırılabilir. Örneğin ünlü Web çerçevesi Gin'deki gin.Context. context standart kütüphanesi de birkaç uygulama sağlar. Bunlar şunlardır:
emptyCtxcancelCtxtimerCtxvalueCtx
Context
Önce Context arayüzünün tanımına bakalım, ardından somut uygulamasını anlayalım.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Deadline
Bu yöntemin iki dönüş değeri vardır. deadline, bağlamın iptal edilmesi gereken son tarihtir. İkinci değer deadline'ın ayarlanıp ayarlanmadığıdır. Ayarlanmamışsa her zaman false'tur.
Deadline() (deadline time.Time, ok bool)Done
Dönüş değeri boş yapı tipi sadece okunabilir kanaldır. Bu kanal sadece bildirim işlevi görür ve hiçbir veri iletmez. Bağlamın yaptığı iş iptal edilmesi gerektiğinde, bu kanal kapatılır. İptal edilemeyen bazı bağlamlar için nil döndürebilir.
Done() <-chan struct{}Err
Bu yöntem bir error döndürür. Bağlamın kapanma nedenini gösterir. Done kanalı kapatılmadığında nil döndürür. Kapatıldıktan sonra, neden kapatıldığını açıklayan bir err döndürür.
Err() errorValue
Bu yöntem ilgili anahtar değerini döndürür. Eğer key bulunamazsa veya yöntem desteklenmiyorsa, nil döndürür.
Value(key any) anyemptyCtx
Adından da anlaşılacağı gibi, emptyCtx boş bağlamdır. context paketi altındaki tüm uygulamalar dışarıya açık değildir. Ancak bağlam oluşturmak için karşılık gelen fonksiyonlar sağlar. emptyCtx, context.Background ve context.TODO kullanılarak oluşturulabilir. İki fonksiyon şu şekildedir
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}Sadece emptyCtx işaretçisi döndürüldüğünü görebilirsiniz. emptyCtx alt türü aslında bir int'tir. Neden boş yapı kullanılmadığı çünkü emptyCtx örneklerinin farklı bellek adreslerine sahip olması gerekir. İptal edilemez, deadline yoktur ve değer alınamaz. Uygulanan yöntemler sıfır değer döndürür.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}emptyCtx genellikle en üst düzey bağlam olarak kullanılır. Diğer üç bağlam oluşturulurken üst bağlam olarak iletilir. context paketindeki çeşitli uygulamaların ilişkisi aşağıdaki şekilde gösterilmiştir

valueCtx
valueCtx uygulaması oldukça basittir. İçinde sadece bir anahtar-değer çifti ve gömülü bir Context türü alanı bulunur.
type valueCtx struct {
Context
key, val any
}Kendisi sadece Value yöntemini uygular. Mantık da çok basittir. Mevcut bağlamda bulunamazsa üst bağlamda arar.
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}Aşağıda valueCtx için basit bir kullanım örneği bulunmaktadır
var waitGroup sync.WaitGroup
func main() {
waitGroup.Add(1)
// Bağlam ilet
go Do(context.WithValue(context.Background(), 1, 2))
waitGroup.Wait()
}
func Do(ctx context.Context) {
// Yeni zamanlayıcı oluştur
ticker := time.NewTimer(time.Second)
defer waitGroup.Done()
for {
select {
case <-ctx.Done(): // Asla çalışmayacak
case <-ticker.C:
fmt.Println("timeout")
return
default:
fmt.Println(ctx.Value(1))
}
time.Sleep(time.Millisecond * 100)
}
}valueCtx çoğunlukla çok düzeyli goroutine'lerde veri iletmek için kullanılır. İptal edilemez, bu nedenle ctx.Done her zaman nil döndürür. select nil kanalını yoksayar. Son çıktı şu şekildedir
2
2
2
2
2
2
2
2
2
2
timeoutcancelCtx
cancelCtx ve timerCtx her ikisi de canceler arayüzünü uygular. Arayüz türü şu şekildedir
type canceler interface {
// removeFromParent, üst bağlamdan kendini silip silmeyeceğini belirtir
// err, iptal nedenini belirtir
cancel(removeFromParent bool, err error)
// Done, iptal nedenini bildirmek için bir kanal döndürür
Done() <-chan struct{}
}cancel yöntemi dışarıya açık değildir. Bağlam oluşturulurken kapama yoluyla dönüş değeri olarak paketlenir. context.WithCancel kaynak kodunda gösterildiği gibi
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
// Kendini üstün children'ına eklemeyi dene
propagateCancel(parent, &c)
// context ve bir fonksiyon döndür
return &c, func() { c.cancel(true, Canceled) }
}cancelCtx, iptal edilebilir bağlam olarak çevrilir. Oluşturulurken, üst canceler uygularsa, kendini üstün children'ına ekler. Aksi takdirde yukarı doğru aramaya devam eder. Tüm üstler canceler uygulamamışsa, bir goroutine başlatır ve üstün iptal edilmesini bekler. Sonra üst bittiğinde mevcut bağlamı iptal eder. cancelFunc çağrıldığında, Done kanalı kapatılır. Bu bağlamın herhangi bir altı da buna göre iptal edilir. Son olarak kendini üstten siler. Aşağıda basit bir örnek bulunmaktadır:
var waitGroup sync.WaitGroup
func main() {
bkg := context.Background()
// Bir cancelCtx ve cancel fonksiyonu döndürür
cancelCtx, cancel := context.WithCancel(bkg)
waitGroup.Add(1)
go func(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("İptal bekleniyor...")
}
time.Sleep(time.Millisecond * 200)
}
}(cancelCtx)
time.Sleep(time.Second)
cancel()
waitGroup.Wait()
}Çıktı şu şekildedir
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
context canceledDaha derin iç içe geçmiş bir örneğe bakalım
var waitGroup sync.WaitGroup
func main() {
waitGroup.Add(3)
ctx, cancelFunc := context.WithCancel(context.Background())
go HttpHandler(ctx)
time.Sleep(time.Second)
cancelFunc()
waitGroup.Wait()
}
func HttpHandler(ctx context.Context) {
cancelCtxAuth, cancelAuth := context.WithCancel(ctx)
cancelCtxMail, cancelMail := context.WithCancel(ctx)
defer cancelAuth()
defer cancelMail()
defer waitGroup.Done()
go AuthService(cancelCtxAuth)
go MailService(cancelCtxMail)
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println("HTTP isteği işleniyor...")
}
time.Sleep(time.Millisecond * 200)
}
}
func AuthService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println("auth üst iptal etti", ctx.Err())
return
default:
fmt.Println("auth...")
}
time.Sleep(time.Millisecond * 200)
}
}
func MailService(ctx context.Context) {
defer waitGroup.Done()
for {
select {
case <-ctx.Done():
fmt.Println("mail üst iptal etti", ctx.Err())
return
default:
fmt.Println("mail...")
}
time.Sleep(time.Millisecond * 200)
}
}Örnekte 3 cancelCtx oluşturuldu. Üst cancelCtx iptal edilirken alt bağlamlarını da iptal etse de, sigorta için bir cancelCtx oluşturulduysa, ilgili işlem bittikten sonra cancel fonksiyonu çağrılmalıdır. Çıktı şu şekildedir
HTTP isteği işleniyor...
auth...
mail...
mail...
auth...
HTTP isteği işleniyor...
auth...
mail...
HTTP isteği işleniyor...
HTTP isteği işleniyor...
auth...
mail...
auth...
HTTP isteği işleniyor...
mail...
context canceled
auth üst iptal etti context canceled
mail üst iptal etti context canceledtimerCtx
timerCtx, cancelCtx temelinde zaman aşımı mekanizması ekler. context paketi iki oluşturma fonksiyonu sağlar. Bunlar WithDeadline ve WithTimeout'tur. Her ikisi de benzer işlevlere sahiptir. İlki belirli bir zaman aşımı süresi belirtir. Örneğin belirli bir具体时间 2023/3/20 16:32:00 gibi. İkincisi bir zaman aşımı aralığı belirtir. Örneğin 5 dakika sonra. İki fonksiyonun imzası şu şekildedir
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)timerCtx, süresi dolduğunda mevcut bağlamı otomatik olarak iptal eder. İptal işlemi timer'ı kapatmanın yanı sıra, temel olarak cancelCtx ile aynıdır. Aşağıda basit bir timerCtx kullanım örneği bulunmaktadır
var wait sync.WaitGroup
func main() {
deadline, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
defer cancel()
wait.Add(1)
go func(ctx context.Context) {
defer wait.Done()
for {
select {
case <-ctx.Done():
fmt.Println("Bağlam iptal edildi", ctx.Err())
return
default:
fmt.Println("İptal bekleniyor...")
}
time.Sleep(time.Millisecond * 200)
}
}(deadline)
wait.Wait()
}Bağlamın süresi dolunca otomatik olarak iptal edilse de, sigorta için ilgili işlem bittikten sonra manuel olarak bağlamı iptal etmek en iyisidir. Çıktı şu şekildedir
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
İptal bekleniyor...
Bağlam iptal edildi context deadline exceededWithTimeout aslında WithDeadline'e çok benzer. Uygulaması da sadece biraz paketleyip WithDeadline'i çağırır. Yukarıdaki örnekteki WithDeadline kullanımı ile aynıdır. Aşağıdaki gibi
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}::: ipucu
Bellek tahsisinden sonra geri alınmaması bellek sızıntısına neden olduğu gibi, bağlam da bir kaynaktır. Oluşturulur ama hiç iptal edilmezse, bağlam sızıntısına neden olur. Bu nedenle bu durumdan kaçınmak en iyisidir.
:::
Select
select, Linux sisteminde bir IO çoklu kullanım çözümüdür. Benzer şekilde, Go'da select bir kanal çoklu kullanım kontrol yapısıdır. Çoklu kullanım nedir, basitçe tek bir cümleyle özetleyelim: Belirli bir anda, birden fazla öğenin kullanılıp kullanılamadığını izler. İzlenen öğeler ağ isteği, dosya IO vb. olabilir. Go'daki select'in izlediği öğe kanaldır ve sadece kanal olabilir. select sözdizimi switch ifadesine benzer. Aşağıda bir select ifadesinin nasıl göründüğüne bakalım
func main() {
// Üç kanal oluştur
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
default:
fmt.Println("Tüm kanallar kullanılamıyor")
}
}Kullanım
switch ile benzer şekilde, select birden fazla case ve bir default'tan oluşur. default dalı atlanabilir. Her case sadece bir kanalı işleyebilir ve sadece bir işlem yapabilir. Ya okuma ya da yazma. Birden fazla case kullanılabildiğinde, select sözde rastgele bir case seçer ve çalıştırır. Tüm case'ler kullanılamıyorsa, default dalı çalıştırılır. default dalı yoksa, en az bir case kullanılana kadar engelleme bekler. Yukarıdaki örnekte kanala veri yazılmadığından, doğal olarak tüm case'ler kullanılamaz. Bu nedenle son çıktı default dalının çalıştırma sonucudur. Biraz değiştirdikten sonra aşağıdaki gibi olur:
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
// Yeni bir goroutine başlat
go func() {
// A kanalına veri yaz
chA <- 1
}()
select {
case n, ok := <-chA:
fmt.Println(n, ok)
case n, ok := <-chB:
fmt.Println(n, ok)
case n, ok := <-chC:
fmt.Println(n, ok)
}
}Yukarıdaki örnek A kanalına veri yazmak için yeni bir goroutine başlatır. select varsayılan dalı olmadığından, case kullanılana kadar sürekli engelleme bekler. A kanalı kullanılabildiğinde, karşılık gelen dal çalıştırıldıktan sonra ana goroutine doğrudan çıkar. Kanalı sürekli izlemek için for döngüsü ile birlikte kullanılabilir. Aşağıdaki gibi.
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
go Send(chA)
go Send(chB)
go Send(chC)
// for döngüsü
for {
select {
case n, ok := <-chA:
fmt.Println("A", n, ok)
case n, ok := <-chB:
fmt.Println("B", n, ok)
case n, ok := <-chC:
fmt.Println("C", n, ok)
}
}
}
func Send(ch chan<- int) {
for i := 0; i < 3; i++ {
time.Sleep(time.Millisecond)
ch <- i
}
}Bu gerçekten üç kanalı da kullanılabilir hale getirir. Ancak sonsuz döngü + select ana goroutine'in sonsuza kadar engellenmesine neden olur. Bu nedenle yeni goroutine'e koyabilir ve bazı diğer mantıklar ekleyebilirsiniz.
func main() {
chA := make(chan int)
chB := make(chan int)
chC := make(chan int)
defer func() {
close(chA)
close(chB)
close(chC)
}()
l := make(chan struct{})
go Send(chA)
go Send(chB)
go Send(chC)
go func() {
Loop:
for {
select {
case n, ok := <-chA:
fmt.Println("A", n, ok)
case n, ok := <-chB:
fmt.Println("B", n, ok)
case n, ok := <-chC:
fmt.Println("C", n, ok)
case <-time.After(time.Second): // 1 saniyelik zaman aşımı süresi ayarla
break Loop // Döngüden çık
}
}
l <- struct{}{} // Ana goroutine'e çıkabileceğini söyle
}()
<-l
}
func Send(ch chan<- int) {
for i := 0; i < 3; i++ {
time.Sleep(time.Millisecond)
ch <- i
}
}Yukarıdaki örnekte for döngüsü select ile birlikte üç kanalı sürekli izlemek için kullanılır. Dördüncü case bir zaman aşımı kanalıdır. Zaman aşımından sonra döngüden çıkar ve alt goroutine'i bitirir. Son çıktı şu şekildedir
C 0 true
A 0 true
B 0 true
A 1 true
B 1 true
C 1 true
B 2 true
C 2 true
A 2 trueZaman Aşımı
Önceki örnekte time.After fonksiyonu kullanıldı. Dönüş değeri sadece okunabilir bir kanaldır. Bu fonksiyon select ile birlikte kullanılarak çok basit bir şekilde zaman aşımı mekanizması uygulanabilir. Örnek aşağıdaki gibidir
func main() {
chA := make(chan int)
defer close(chA)
go func() {
time.Sleep(time.Second * 2)
chA <- 1
}()
select {
case n := <-chA:
fmt.Println(n)
case <-time.After(time.Second):
fmt.Println("Zaman aşımı")
}
}Sonsuz Engelleme
select ifadesinde hiçbir şey olmadığında, sonsuza kadar engellenir. Örneğin
func main() {
fmt.Println("start")
select {}
fmt.Println("end")
}end asla çıktılanmaz. Ana goroutine sürekli engellenir. Bu durum genellikle özel amaçlar için kullanılır.
::: ipucu
select'in case'inde nil kanalı üzerinde işlem yapmak, engellemeye neden olmaz. Bu case yoksayılır ve asla çalıştırılmaz. Aşağıdaki kod kaç kez çalıştırılırsa çalıştırılsın sadece timeout çıktılanır.
func main() {
var nilCh chan int
select {
case <-nilCh:
fmt.Println("read")
case nilCh <- 1:
fmt.Println("write")
case <-time.After(time.Second):
fmt.Println("timeout")
}
}:::
Engellemesiz
select'in default dalı kanal ile birlikte kullanılarak engellemesiz gönderme/alma işlemleri uygulanabilir. Aşağıda gösterildiği gibi
func TrySend(ch chan int, ele int) bool {
select {
case ch <- ele:
return true
default:
return false
}
}
func TryRecv(ch chan int) (int, bool) {
select {
case ele, ok := <-ch:
return ele, ok
default:
return 0, false
}
}Aynı şekilde, bir context'in bitip bitmediğini engellemesiz判断也可以实现
func IsDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}Kilit
Önce bir örneğe bakalım
var wait sync.WaitGroup
var count = 0
func main() {
wait.Add(10)
for i := 0; i < 10; i++ {
go func(data *int) {
// Erişim gecikmesi simülasyonu
time.Sleep(time.Millisecond * time.Duration(rand.Intn(5000)))
// Veriye eriş
temp := *data
// Hesaplama gecikmesi simülasyonu
time.Sleep(time.Millisecond * time.Duration(rand.Intn(5000)))
ans := 1
// Veriyi değiştir
*data = temp + ans
fmt.Println(*data)
wait.Done()
}(&count)
}
wait.Wait()
fmt.Println("Son sonuç", count)
}Yukarıdaki örnek için, count üzerinde +1 işlemi yapmak için on goroutine başlatıldı. Ve farklı gecikmeleri simüle etmek için time.Sleep kullanıldı. Sezgimize göre, 10 goroutine 10 +1 işlemi yaparsa, son sonuç kesinlikle 10 olmalıdır. Doğru sonuç da gerçekten 10'dur. Ancak gerçekte böyle değildir. Yukarıdaki örnek çalıştırma sonucu şu şekildedir:
1
2
3
3
2
2
3
3
3
4
Son sonuç 4Sonucun 4 olduğunu görebilirsiniz. Bu sadece birçok olası sonuçtan biridir. Her goroutine'in veri erişimi ve hesaplama için harcadığı zaman farklı olduğundan, A goroutine'i veri erişimi için 500 milisaniye harcar. Bu sırada erişilen count değeri 1'dir. Ardından 400 milisaniye hesaplama harcar. Ancak bu 400 milisaniye içinde, B goroutine'i zaten erişimi ve hesaplamayı tamamlamış ve count değerini başarıyla güncellemiştir. A goroutine'i hesaplamayı bitirdikten sonra, A goroutine'inin başlangıçta eriştiği değer eski olmuştur. Ancak A goroutine'i bunu bilmez ve hala başlangıçta eriştiği değere bir ekler ve count'a atar. Bu şekilde, B goroutine'inin çalıştırma sonucu üzerine yazılır. Birden fazla goroutine bir paylaşılan veriyi okuyup eriştiğinde, özellikle bu tür sorunlar oluşur. Bunun için kilide ihtiyaç vardır.
Go'da sync paketi altındaki Mutex ve RWMutex karşılıklı dışlama kilidi ve okuma-yazma kilidi olmak üzere iki uygulama sağlar. Ve çok basit ve kullanışlı API'ler sağlar. Kilitleme sadece Lock() gerektirir ve kilidi açma da sadece Unlock() gerektirir. Unutulmaması gereken, Go'nun sağladığı kilitlerin özyinelemeli olmayan kilitler olduğudur. Yani yeniden girişli olmayan kilitlerdir. Bu nedenle tekrar kilitleme veya tekrar kilidi açma fatal'a neden olur. Kilidin anlamı değişmezleri korumaktır. Kilitleme, verinin diğer goroutine'ler tarafından değiştirilmemesini umar. Aşağıdaki gibi
func DoSomething() {
Lock()
// Bu süreçte, veri diğer goroutine'ler tarafından değiştirilmez
Unlock()
}Eğer özyinelemeli kilit olsaydı, aşağıdaki durum oluşabilirdi
func DoSomething() {
Lock()
DoOther()
Unlock()
}
func DoOther() {
Lock()
// diğerini yap
Unlock()
}DoSomething fonksiyonu açıkça DoOther fonksiyonunun veriye ne yapabileceğini bilmez ve böylece veriyi değiştirir. Örneğin birkaç alt goroutine daha açarak değişmezi bozar. Bu Go'da çalışmaz. Bir kez kilitlendikten sonra değişmezin değişmezliğini garanti etmelidir. Bu sırada tekrar kilitleme ve kilidi açma deadlock'a neden olur. Bu nedenle kod yazarken yukarıdaki durumdan kaçınmalısınız. Gerekli olduğunda kilitleme ile birlikte hemen defer ifadesi kullanarak kilidi açın.
Karşılıklı Dışlama Kilidi
sync.Mutex, Go tarafından sağlanan karşılıklı dışlama kilidi uygulamasıdır. sync.Locker arayüzünü uygular
type Locker interface {
// Kilitle
Lock()
// Kilidi aç
Unlock()
}Karşılıklı dışlama kilidi kullanarak yukarıdaki sorunu mükemmel bir şekilde çözebilirsiniz. Örnek aşağıdaki gibidir
var wait sync.WaitGroup
var count = 0
var lock sync.Mutex
func main() {
wait.Add(10)
for i := 0; i < 10; i++ {
go func(data *int) {
// Kilitle
lock.Lock()
// Erişim gecikmesi simülasyonu
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
// Veriye eriş
temp := *data
// Hesaplama gecikmesi simülasyonu
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
ans := 1
// Veriyi değiştir
*data = temp + ans
// Kilidi aç
lock.Unlock()
fmt.Println(*data)
wait.Done()
}(&count)
}
wait.Wait()
fmt.Println("Son sonuç", count)
}Her goroutine veriye erişmeden önce kilitleme yapar. Güncelleme bittikten sonra kilidi açar. Diğer goroutine'ler erişmek istiyorsa önce kilidi almalıdır. Aksi takdirde engelleme bekler. Bu şekilde, yukarıdaki sorun oluşmaz. Bu nedenle çıktı şu şekildedir
1
2
3
4
5
6
7
8
9
10
Son sonuç 10Okuma-Yazma Kilidi
Karşılıklı dışlama kilidi, okuma ve yazma işlemlerinin sıklığının benzer olduğu durumlar için uygundur. Okuma işlemlerinin yazma işlemlerinden çok daha fazla olduğu veriler için, karşılıklı dışlama kilidi kullanılırsa, çok sayıda gereksiz goroutine kilidi rekabeti oluşur. Bu çok fazla sistem kaynağı tüketir. Bu sırada okuma-yazma kilidine ihtiyaç vardır. Yani okuma-yazma karşılıklı dışlama kilidi. Bir goroutine için:
- Eğer okuma kilidi alınırsa, diğer goroutine'ler yazma işlemi yaparken engellenir. Diğer goroutine'ler okuma işlemi yaparken engellenmez
- Eğer yazma kilidi alınırsa, diğer goroutine'ler yazma işlemi yaparken engellenir. Diğer goroutine'ler okuma işlemi yaparken engellenir
Go'da okuma-yazma karşılıklı dışlama kilidi uygulaması sync.RWMutex'tir. Aynı zamanda Locker arayüzünü uygular. Ancak daha fazla kullanılabilir yöntem sağlar. Aşağıdaki gibi:
// Okuma kilidi al
func (rw *RWMutex) RLock()
// Okuma kilidi almayı dene
func (rw *RWMutex) TryRLock() bool
// Okuma kilidini aç
func (rw *RWMutex) RUnlock()
// Yazma kilidi al
func (rw *RWMutex) Lock()
// Yazma kilidi almayı dene
func (rw *RWMutex) TryLock() bool
// Yazma kilidini aç
func (rw *RWMutex) Unlock()TryRlock ve TryLock iki deneme kilitleme işlemi engellemesizdir. Başarılı kilitleme true döndürür. Kilit alınamadığında engellemez, bunun yerine false döndürür. Okuma-yazma karşılıklı dışlama kilidi iç uygulaması hala karşılıklı dışlama kilididir. Okuma kilidi ve yazma kilidi diye iki kilit olduğu anlamına gelmez. Başından sonuna kadar sadece bir kilit vardır. Aşağıda okuma-yazma karşılıklı dışlama kilidi kullanım örneğine bakalım
var wait sync.WaitGroup
var count = 0
var rw sync.RWMutex
func main() {
wait.Add(12)
// Çok okuma az yazma
go func() {
for i := 0; i < 3; i++ {
go Write(&count)
}
wait.Done()
}()
go func() {
for i := 0; i < 7; i++ {
go Read(&count)
}
wait.Done()
}()
// Alt goroutine'lerin bitmesini bekle
wait.Wait()
fmt.Println("Son sonuç", count)
}
func Read(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
rw.RLock()
fmt.Println("Okuma kilidi alındı")
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println("Okuma kilidi açıldı", *i)
rw.RUnlock()
wait.Done()
}
func Write(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
rw.Lock()
fmt.Println("Yazma kilidi alındı")
temp := *i
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
*i = temp + 1
fmt.Println("Yazma kilidi açıldı", *i)
rw.Unlock()
wait.Done()
}Bu örnek 3 yazma goroutine'i ve 7 okuma goroutine'i başlatır. Veri okurken önce okuma kilidi alırlar. Okuma goroutine'leri normal olarak okuma kilidi alabilir. Ancak yazma goroutine'lerini engeller. Yazma kilidi alındığında, hem okuma goroutine'lerini hem de yazma goroutine'lerini engeller. Yazma kilidi açılana kadar. Bu şekilde okuma goroutine'leri ile yazma goroutine'leri karşılıklı dışlama sağlar ve verinin doğruluğunu garanti eder. Örnek çıktısı şu şekildedir:
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi açıldı 0
Okuma kilidi açıldı 0
Okuma kilidi açıldı 0
Okuma kilidi açıldı 0
Yazma kilidi alındı
Yazma kilidi açıldı 1
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi açıldı 1
Okuma kilidi açıldı 1
Okuma kilidi açıldı 1
Yazma kilidi alındı
Yazma kilidi açıldı 2
Yazma kilidi alındı
Yazma kilidi açıldı 3
Son sonuç 3::: ipucu
Kilit için, değer olarak iletilmemeli ve saklanmamalıdır. İşaretçi kullanılmalıdır.
:::
Koşul Değişkeni
Koşul değişkeni, karşılıklı dışlama kilidi ile birlikte ortaya çıkar ve kullanılır. Bu nedenle bazı kişiler yanlışlıkla koşul kilidi olarak adlandırabilir. Ancak kilit değildir, bir iletişim mekanizmasıdır. Go'da sync.Cond bunun için uygulama sağlar. Koşul değişkeni oluşturma fonksiyonunun imzası şu şekildedir:
func NewCond(l Locker) *CondBir koşul değişkeni oluşturmanın ön koşulunun bir kilit oluşturmak olduğunu görebilirsiniz. sync.Cond kullanım için aşağıdaki yöntemleri sağlar
// Koşul geçerli olana kadar engelleme bekle, uyanana kadar
func (c *Cond) Wait()
// Koşul nedeniyle engellenen bir goroutine'i uyandır
func (c *Cond) Signal()
// Koşul nedeniyle engellenen tüm goroutine'leri uyandır
func (c *Cond) Broadcast()Koşul değişkeni kullanımı çok basittir. Yukarıdaki okuma-yazma karşılıklı dışlama kilidi örneğini biraz değiştirerek kullanılabilir
var wait sync.WaitGroup
var count = 0
var rw sync.RWMutex
// Koşul değişkeni
var cond = sync.NewCond(rw.RLocker())
func main() {
wait.Add(12)
// Çok okuma az yazma
go func() {
for i := 0; i < 3; i++ {
go Write(&count)
}
wait.Done()
}()
go func() {
for i := 0; i < 7; i++ {
go Read(&count)
}
wait.Done()
}()
// Alt goroutine'lerin bitmesini bekle
wait.Wait()
fmt.Println("Son sonuç", count)
}
func Read(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)))
rw.RLock()
fmt.Println("Okuma kilidi alındı")
// Koşul karşılanmazsa sürekli engelle
for *i < 3 {
cond.Wait()
}
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
fmt.Println("Okuma kilidi açıldı", *i)
rw.RUnlock()
wait.Done()
}
func Write(i *int) {
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
rw.Lock()
fmt.Println("Yazma kilidi alındı")
temp := *i
time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
*i = temp + 1
fmt.Println("Yazma kilidi açıldı", *i)
rw.Unlock()
// Koşul değişkeni nedeniyle engellenen tüm goroutine'leri uyandır
cond.Broadcast()
wait.Done()
}Koşul değişkeni oluştururken, burada koşul değişkeni okuma goroutine'leri üzerinde çalıştığından, okuma kilidi karşılıklı dışlama kilidi olarak iletilir. Doğrudan okuma-yazma karşılıklı dışlama kilidi iletilirse, yazma goroutine'leri tekrar kilidi açma sorununa neden olur. Burada sync.rlocker iletilir. RWMutex.RLocker yöntemi ile alınır.
func (rw *RWMutex) RLocker() Locker {
return (*rlocker)(rw)
}
type rlocker RWMutex
func (r *rlocker) Lock() { (*RWMutex)(r).RLock() }
func (r *rlocker) Unlock() { (*RWMutex)(r).RUnlock() }rlocker'ın sadece okuma-yazma karşılıklı dışlama kilidinin okuma kilidi işlemini paketlediğini görebilirsiniz. Aslında aynı referanstır ve hala aynı kilittir. Okuma goroutine'leri veri okurken, 3'ten küçükse sürekli engelleme bekler. Veri 3'ten büyük olana kadar. Yazma goroutine'leri veriyi güncelledikten sonra koşul değişkeni nedeniyle engellenen tüm goroutine'leri uyandırmayı dener. Bu nedenle son çıktı şu şekildedir
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi alındı
Okuma kilidi alındı
Yazma kilidi alındı
Yazma kilidi açıldı 1
Okuma kilidi alındı
Yazma kilidi alındı
Yazma kilidi açıldı 2
Okuma kilidi alındı
Okuma kilidi alındı
Yazma kilidi alındı
Yazma kilidi açıldı 3 // Üçüncü yazma goroutine'i çalışmayı bitirdi
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Okuma kilidi açıldı 3
Son sonuç 3Sonuçtan görülebileceği gibi, üçüncü yazma goroutine'i veriyi güncelledikten sonra, yedi okuma goroutine'i koşul değişkeni nedeniyle engellenmişti ve hepsi çalışmaya devam etti.
::: ipucu
Koşul değişkeni için, if yerine for kullanılmalıdır. Koşulun karşılanıp karşılanmadığını判断 etmek için döngü kullanılmalıdır. Çünkü goroutine uyandığında mevcut koşulun karşılandığı garanti edilemez.
for !condition {
cond.Wait()
}:::
sync
Go'da eşzamanlılıkla ilgili araçların çoğu sync standart kütüphanesi tarafından sağlanır. Yukarıda zaten sync.WaitGroup, sync.Locker vb. tanıtıldı. Bunun dışında, sync paketi altında kullanılabilecek başka araçlar da vardır.
Once
Bazı veri yapılarını kullanırken, bu veri yapıları çok büyükse, tembel yükleme yöntemi düşünülebilir. Yani gerçekten kullanılması gerektiğinde veri yapısı başlatılır. Aşağıdaki örnek gibi
type MySlice []int
func (m *MySlice) Get(i int) (int, bool) {
if *m == nil {
return 0, false
} else {
return (*m)[i], true
}
}
func (m *MySlice) Add(i int) {
// Slice gerçekten kullanıldığında başlatmayı düşün
if *m == nil {
*m = make([]int, 0, 10)
}
*m = append(*m, i)
}Sorun şu ki, sadece bir goroutine kullanıyorsa kesinlikle sorun yoktur. Ancak birden fazla goroutine erişirse sorun oluşabilir. Örneğin goroutine A ve B aynı anda Add yöntemini çağırır. A biraz daha hızlı çalışır, başlatmayı tamamlar ve veriyi başarıyla ekler. Ardından goroutine B tekrar başlatır. Bu şekilde goroutine A'nın eklediği veri doğrudan üzerine yazılır. İşte sorun budur.
sync.Once'ın çözmek istediği sorun budur. Adından da anlaşılacağı gibi, Once bir kez anlamına gelir. sync.Once, eşzamanlılık koşullarında belirtilen işlemin sadece bir kez çalıştırılacağını garanti eder. Kullanımı çok basittir, sadece bir Do yöntemini dışarıya açar. İmza şu şekildedir:
func (o *Once) Do(f func())Kullanırken, başlatma işlemini Do yöntemine iletmeniz yeterlidir. Aşağıdaki gibi
var wait sync.WaitGroup
func main() {
var slice MySlice
wait.Add(4)
for i := 0; i < 4; i++ {
go func() {
slice.Add(1)
wait.Done()
}()
}
wait.Wait()
fmt.Println(slice.Len())
}
type MySlice struct {
s []int
o sync.Once
}
func (m *MySlice) Get(i int) (int, bool) {
if m.s == nil {
return 0, false
} else {
return m.s[i], true
}
}
func (m *MySlice) Add(i int) {
// Slice gerçekten kullanıldığında başlatmayı düşün
m.o.Do(func() {
fmt.Println("Başlatma")
if m.s == nil {
m.s = make([]int, 0, 10)
}
})
m.s = append(m.s, i)
}
func (m *MySlice) Len() int {
return len(m.s)
}Çıktı şu şekildedir
Başlatma
4Çıktı sonucundan görülebileceği gibi, tüm veriler normal olarak slice'e eklenir ve başlatma işlemi sadece bir kez çalıştırılır. Aslında sync.Once uygulaması oldukça basittir. Yorumlar kaldırıldığında gerçek kod mantığı sadece 16 satırdır. Prensibi kilit + atomik işlemdir. Kaynak kodu şu şekildedir:
type Once struct {
// İşlemin çalıştırılıp çalıştırılmadığını判断 etmek için kullanılır
done uint32
m Mutex
}
func (o *Once) Do(f func()) {
// Atomik yükleme verisi
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
// Kilitle
o.m.Lock()
// Kilidi aç
defer o.m.Unlock()
// Çalıştırılıp çalıştırılmadığını判断 et
if o.done == 0 {
// Çalıştırma bittikten sonra done'ı değiştir
defer atomic.StoreUint32(&o.done, 1)
f()
}
}Pool
sync.Pool tasarım amacı, geçici nesneleri depolamak ve sonraki yeniden kullanım içindir. Geçici bir eşzamanlı güvenli nesne havuzudur. Geçici olarak kullanılmayan nesneler havuza konur. Sonraki kullanımda ekstra nesne oluşturmak yerine doğrudan yeniden kullanılabilir. Bellek tahsisi ve serbest bırakma sıklığını azaltır. En önemlisi GC baskısını azaltır. sync.Pool toplamda sadece iki yönteme sahiptir. Aşağıdaki gibi:
// Bir nesne talep et
func (p *Pool) Get() any
// Bir nesne koy
func (p *Pool) Put(x any)Ve sync.Pool'un dışarıya açık bir New alanı vardır. Nesne havuzu nesne talep edemediğinde bir nesne başlatmak için kullanılır
New func() anyAşağıda bir örnek ile gösterilmektedir
var wait sync.WaitGroup
// Geçici nesne havuzu
var pool sync.Pool
// Toplamda kaç nesne oluşturulduğunu saymak için kullanılır
var numOfObject atomic.Int64
// BigMemData, büyük bellek kaplayan bir yapı olduğunu varsayalım
type BigMemData struct {
M string
}
func main() {
pool.New = func() any {
numOfObject.Add(1)
return BigMemData{"Büyük bellek"}
}
wait.Add(1000)
// Burada 1000 goroutine başlatılıyor
for i := 0; i < 1000; i++ {
go func() {
// Nesne talep et
val := pool.Get()
// Nesneyi kullan
_ = val.(BigMemData)
// Kullanım bittikten sonra nesneyi havuza geri koy
pool.Put(val)
wait.Done()
}()
}
wait.Wait()
fmt.Println(numOfObject.Load())
}Örnekte 1000 goroutine sürekli olarak havuzdan nesne talep eder ve serbest bırakır. Nesne havuzu kullanılmazsa, 1000 goroutine'in her biri nesneyi örneklemelidir. Ve bu 1000 örneklenen nesne kullanım bittikten sonra GC tarafından bellek serbest bırakılmalıdır. On binlerce goroutine varsa veya nesne oluşturma maliyeti çok yüksekse, bu durumda çok fazla bellek kaplar ve GC'ye çok fazla baskı yapar. Nesne havuzu kullanıldıktan sonra, nesneler yeniden kullanılabilir ve örnekleme sıklığı azaltılabilir. Yukarıdaki örnek çıktısı şu şekilde olabilir:
51000 goroutine başlatılsa bile, tüm süreçte sadece 5 nesne oluşturulur. Nesne havuzu kullanılmazsa 1000 goroutine 1000 nesne oluşturur. Bu optimizasyonun getirdiği iyileştirme açıktır. Özellikle eşzamanlılık çok büyük olduğunda ve nesne örnekleme maliyeti çok yüksek olduğunda avantaj daha belirgindir.
sync.Pool kullanırken birkaç noktaya dikkat edilmelidir:
- Geçici nesne:
sync.Poolsadece geçici nesneleri depolamak için uygundur. Havuzdaki nesneler herhangi bir bildirim olmadan GC tarafından kaldırılabilir. Bu nedenle ağ bağlantısı, veritabanı bağlantısı gibi nesnelerinsync.Pool'a konması önerilmez. - Tahmin edilemez:
sync.Poolnesne talep ederken, nesnenin yeni oluşturulan mı yoksa yeniden kullanılan mı olduğunu tahmin edemez. Havuzda kaç nesne olduğunu da bilemez - Eşzamanlı güvenlik: Resmi
sync.Pool'un kesinlikle eşzamanlı güvenli olduğunu garanti eder. Ancak nesne oluşturmak için kullanılanNewfonksiyonunun eşzamanlı güvenli olduğunu garanti etmez.Newfonksiyonu kullanıcı tarafından iletilir. Bu nedenleNewfonksiyonunun eşzamanlı güvenliği kullanıcı tarafından korunmalıdır. Yukarıdaki örnekte nesne sayımının atomik değer kullanmasının nedeni de budur.
::: ipucu
Son olarak dikkat edilmesi gereken, nesne kullanıldıktan sonra mutlaka havuza geri konulmalıdır. Kullanıldıktan sonra serbest bırakılmazsa, nesne havuzunun kullanımı anlamsız olur.
:::
Standart kütüphane fmt paketi altında bir nesne havuzu kullanım örneği vardır. fmt.Fprintf fonksiyonunda
func Fprintf(w io.Writer, format string, a ...any) (n int, err error) {
// Bir yazdırma arabelleği talep et
p := newPrinter()
p.doPrintf(format, a)
n, err = w.Write(p.buf)
// Kullanım bittikten sonra serbest bırak
p.free()
return
}newPointer fonksiyonu ve free yönteminin uygulaması şu şekildedir
func newPrinter() *pp {
// Nesne havuzundan bir nesne talep et
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
func (p *pp) free() {
// Nesne havuzundaki arabellek boyutunun yaklaşık olarak aynı olması için
// Daha iyi esnek kontrol için çok büyük arabellekler nesne havuzuna geri konmaz
if cap(p.buf) > 64<<10 {
return
}
// Alanlar sıfırlandıktan sonra nesneyi havuza serbest bırak
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}Map
sync.Map, resmi tarafından sağlanan eşzamanlı güvenli Map uygulamasıdır. Kutudan çıkar çıkmaz kullanılabilir ve kullanımı çok basittir. Aşağıda bu yapının dışarıya açık yöntemleri bulunmaktadır:
// Bir anahtara göre değer oku, dönüş değeri karşılık gelen değeri ve değerin var olup olmadığını döndürür
func (m *Map) Load(key any) (value any, ok bool)
// Bir anahtar-değer çifti sakla
func (m *Map) Store(key, value any)
// Bir anahtar-değer çiftini sil
func (m *Map) Delete(key any)
// Eğer anahtar zaten varsa, mevcut değeri döndür. Aksi takdirde yeni değeri sakla ve döndür. Değer başarıyla okunduğunda, loaded true olur, aksi takdirde false olur
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)
// Bir anahtar-değer çiftini sil ve mevcut değerini döndür. loaded değeri anahtarın var olup olmadığına bağlıdır
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)
// Map'i yinele, f() false döndürdüğünde, yinelemeyi durdur
func (m *Map) Range(f func(key, value any) bool)Aşağıda sync.Map temel kullanımını gösteren basit bir örnek bulunmaktadır
func main() {
var syncMap sync.Map
// Veri sakla
syncMap.Store("a", 1)
syncMap.Store("a", "a")
// Veri oku
fmt.Println(syncMap.Load("a"))
// Oku ve sil
fmt.Println(syncMap.LoadAndDelete("a"))
// Oku veya sakla
fmt.Println(syncMap.LoadOrStore("a", "hello world"))
syncMap.Store("b", "goodbye world")
// Map'i yinele
syncMap.Range(func(key, value any) bool {
fmt.Println(key, value)
return true
})
}Çıktı
a true
a true
hello world false
a hello world
b goodbye worldArdından eşzamanlı map kullanım örneğine bakalım:
func main() {
myMap := make(map[int]int, 10)
var wait sync.WaitGroup
wait.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
for i := 0; i < 100; i++ {
myMap[n] = n
}
wait.Done()
}(i)
}
wait.Wait()
}Yukarıdaki örnekte normal map kullanılır. 10 goroutine sürekli veri saklar. Açıkçası bu büyük olasılıkla fatal'ı tetikler. Sonuç muhtemelen şu şekildedir
fatal error: concurrent map writessync.Map kullanarak bu sorundan kaçınabilirsiniz
func main() {
var syncMap sync.Map
var wait sync.WaitGroup
wait.Add(10)
for i := 0; i < 10; i++ {
go func(n int) {
for i := 0; i < 100; i++ {
syncMap.Store(n, n)
}
wait.Done()
}(i)
}
wait.Wait()
syncMap.Range(func(key, value any) bool {
fmt.Println(key, value)
return true
})
}Çıktı şu şekildedir
8 8
3 3
1 1
9 9
6 6
5 5
7 7
0 0
2 2
4 4Eşzamanlı güvenlik için kesinlikle bazı fedakarlıklar yapılmalıdır. sync.Map performansı map'ten 10-100 kat daha düşüktür.
Atomik
Bilgisayar biliminde, atom veya ilkel işlem, genellikle daha fazla bölünemeyen işlemleri ifade eder. Bu işlemler daha küçük adımlara bölünemediğinden, çalıştırılmadan önce başka herhangi bir goroutine tarafından kesilemez. Bu nedenle çalıştırma sonucu ya başarılı olur ya da başarısız. Üçüncü bir durum söz konusu değildir. Başka bir durum ortaya çıkarsa, atomik işlem değildir. Aşağıdaki kod örneği:
func main() {
a := 0
if a == 0 {
a = 1
}
fmt.Println(a)
}Yukarıdaki kod basit bir判断 dalıdır. Kod çok az olsa da, atomik işlem değildir. Gerçek atomik işlem donanım komut düzeyi tarafından desteklenir.
Tür
Neyse ki çoğu durumda汇编 yazmaya gerek yoktur. Go standart kütüphanesi sync/atomic paketi atomik işlemle ilgili API'ler sağlar. Atomik işlem için aşağıdaki türleri sağlar.
atomic.Bool{}
atomic.Pointer[]{}
atomic.Int32{}
atomic.Int64{}
atomic.Uint32{}
atomic.Uint64{}
atomic.Uintptr{}
atomic.Value{}Pointer atomik türü jenerikleri destekler. Value türü herhangi bir türü depolamayı destekler. Bunun dışında, işlemi kolaylaştırmak için birçok fonksiyon sağlar. Atomik işlemin granülaritesi çok ince olduğundan, çoğu durumda bu temel veri türlerini işlemek için daha uygundur.
::: ipucu
atomic paketi altındaki atomik işlemler sadece fonksiyon imzasına sahiptir, somut uygulama yoktur. Somut uygulama plan9汇编 ile yazılmıştır.
:::
Kullanım
Her atomik tür aşağıdaki üç yöntemi sağlar:
Load(): Atomik olarak değer alSwap(newVal type) (old type): Atomik olarak değer değiştir ve eski değeri döndürStore(val type): Atomik olarak değer sakla
Farklı türler için başka ek yöntemler de olabilir. Örneğin tamsayı türleri atomik toplama/çıkarma işlemi için Add yöntemi sağlar. Aşağıda int64 türü örneği gösterilmektedir:
func main() {
var aint64 atomic.Uint64
// Değer sakla
aint64.Store(64)
// Değer değiştir
aint64.Swap(128)
// Artır
aint64.Add(112)
// Değer yükle
fmt.Println(aint64.Load())
}Veya doğrudan fonksiyon kullanabilirsiniz
func main() {
var aint64 int64
// Değer sakla
atomic.StoreInt64(&aint64, 64)
// Değer değiştir
atomic.SwapInt64(&aint64, 128)
// Artır
atomic.AddInt64(&aint64, 112)
// Yükle
fmt.Println(atomic.LoadInt64(&aint64))
}Diğer türlerin kullanımı da çok benzerdir. Son çıktı şu şekildedir:
240CAS
atomic paketi CompareAndSwap işlemi sağlar. Yani CAS. İyimser kilit ve kilitsiz veri yapısı uygulamasının çekirdeğidir. İyimser kilit kendisi bir kilit değildir. Eşzamanlılık koşullarında kilitsiz eşzamanlılık kontrol yöntemidir: İş parçacığı/goroutine veriyi değiştirmeden önce, önce kilitlemez. Bunun yerine önce veriyi okur, hesaplama yapar. Ardından değişikliği gönderirken CAS kullanarak bu sırada başka bir iş parçacığının veriyi değiştirip değiştirmediğini判断 eder. Değiştirilmediyse (değer hala önce okunan değere eşitse), değişiklik başarılı olur. Aksi takdirde başarısız olur ve yeniden dener. Bu nedenle iyimser kilit olarak adlandırılmasının nedeni, her zaman paylaşılan verinin değiştirilmeyeceğini iyimser bir şekilde varsaymasıdır. Sadece verinin değiştirilmediği bulunduğunda karşılık gelen işlemi yapar. Önce öğrenilen karşılıklı dışlama miktarı ise kötümser kilittir. Karşılıklı dışlama miktarı her zaman paylaşılan verinin değiştirileceğini kötümser bir şekilde varsayar. Bu nedenle işlem sırasında kilitleme yapar ve işlem bittikten sonra kilidi açar. Kilitsiz uygulamanın eşzamanlılığı, kilide göre güvenliği ve verimliliği daha yüksektir. Birçok eşzamanlı güvenli veri yapısı CAS kullanarak uygulanır. Ancak gerçek verimlilik belirli kullanım senaryosuna bağlıdır. Aşağıdaki örneğe bakalım:
var lock sync.Mutex
var count int
func Add(num int) {
lock.Lock()
count += num
lock.Unlock()
}Bu karşılıklı dışlama kilidi kullanan bir örnektir. Her sayı artırılmadan önce kilitlenir ve çalıştırma bittikten sonra kilidi açılır. Süreçte diğer goroutine'lerin engellenmesine neden olur. Ardından CAS kullanarak dönüştürelim:
var count int64
func Add(num int64) {
for {
expect := atomic.LoadInt64(&count)
if atomic.CompareAndSwapInt64(&count, expect, expect+num) {
break
}
}
}CAS için üç parametre vardır. Bellek değeri, beklenen değer, yeni değer. Çalıştırılırken, CAS beklenen değeri mevcut bellek değeri ile karşılaştırır. Bellek değeri beklenen değerle aynıysa, sonraki işlemi yapar. Aksi takdirde hiçbir şey yapmaz. Go'da atomic paketi altındaki atomik işlemler için, CAS ilgili fonksiyonlar adres, beklenen değer, yeni değer gerektirir. Ve başarılı değiştirme boolean değerini döndürür. Örneğin int64 türü CAS işlem fonksiyon imzası şu şekildedir:
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)CAS örneğinde, önce LoadInt64 kullanarak beklenen değeri alırız. Ardından CompareAndSwapInt64 kullanarak karşılaştırma ve değiştirme yaparız. Başarısız olursa sürekli döngü yaparız, başarılı olana kadar. Bu kilitsiz işlem goroutine'i engellemez. Ancak sürekli döngü CPU için azımsanmayacak bir yük oluşturur. Bu nedenle bazı uygulamalarda başarısızlık belirli bir sayıya ulaştığında işlem vazgeçilebilir. Ancak yukarıdaki işlem için, sadece basit sayı toplama söz konusudur,涉及 işlem karmaşık değildir. Bu nedenle kilitsiz uygulama düşünülebilir.
::: ipucu
Çoğu durumda, sadece değeri karşılaştırmak eşzamanlı güvenlik sağlayamaz. Örneğin CAS'ın neden olduğu ABA sorunu, version eklenerek çözülmelidir.
:::
Value
atomic.Value yapısı, herhangi bir türde değer depolayabilir. Yapı şu şekildedir
type Value struct {
// any türü
v any
}Herhangi bir türü depolayabilse de, nil depolayamaz. Ve前后 depolanan değerlerin türü tutarlı olmalıdır. Aşağıdaki iki örnek derleme başarısız olur
func main() {
var val atomic.Value
val.Store(nil)
fmt.Println(val.Load())
}
// panic: sync/atomic: store of nil value into Valuefunc main() {
var val atomic.Value
val.Store("hello world")
val.Store(114514)
fmt.Println(val.Load())
}
// panic: sync/atomic: store of inconsistently typed value into ValueBunun dışında, kullanımı diğer atomik türlerden çok farklı değildir. Dikkat edilmesi gereken, tüm atomik türler değer kopyalamamalıdır. Bunun yerine işaretçileri kullanılmalıdır.
