İçeriğe geç
academia.sh

Ders 13 / 14

Eşzamanlılık İlkelleri

Otuz goroutine aynı ilklendirmeyi çağırıyor ama bir kez çalıştırma sözleşmesi gövdenin tam olarak bir kez koşmasını garanti ediyor. Otuz goroutine, her biri beş yüz artışla, aynı sayacı bir kez atomik işlemlerle bir kez kilitle artırıyor; ikisi de 15000 veriyor. Sınırlayıcı ölçüm, iki ayrı atomik değişkenin (bakiye ve işlem sayısı) her biri kendi başına tutarlı kalırken bir arada okunduklarında tutarsız bir çift verebileceğini kanalla zorlanmış bir sırayla modelliyor: okuyan taraf bakiye=100, işlemSayısı=0 görüyor.

İçindekiler

Önceki dersin kapanışı bir sınır çizdi: kilit iki ders boyunca paylaşılan durumu korumanın tek aracıymış gibi göründü, ama değil. Bu ders aynı korumayı iki ayrı ilkelle kuruyor — sync.Once, bir gövdenin kaç kez çağrılırsa çağrılsın yalnız bir kez koşmasını garanti eden bir kez çalıştırma; ve sync/atomic, tek bir değişken üzerindeki işlemleri kilit kullanmadan güvenli kılan atomik işlemler. İkisi de kilidin yaptığı işi yapıyor gibi görünüyor, ama ikisinin de sözleşmesi kilitten daha dar: kilit herhangi bir kod parçasını sarabiliyordu, bu iki ilkelin her biri yalnız tek bir dar durumu kapsıyor.

Ortak tanımın üçüncü iddiası bu derste de geçerli kalıyor: durduran şey aracın adı değil, başka bir goroutine’in attığı bir adım. Bir kez çalıştırmada bu adım ilk çağrının gövdeyi bitirmesi — bekleyen goroutine’ler kilitteki gibi beklediği geldi sınıfına giriyor. Atomik işlemde ise hiç bekleme yok: atomik bir çağrı hiçbir goroutine’in bir şey yapmasını beklemeden, hiç bloklanmadan döner — kendi başına biten bir adım, kimseye bağlı değil. İkisi de kilidin tek bir dar biçimini temsil ediyor, ama biri hâlâ bekletiyor, öbürü hiç bekletmiyor.

Bir Kez Çalıştırma

Bir programın başlangıcında yalnız bir kez çalışması gereken bir adım — bir bağlantı havuzunu kurmak, bir yapılandırmayı okumak — birden çok goroutine tarafından aynı anda istenebilir. sync.Once bu isteği tek bir çağrıya indiriyor: Do yöntemine verilen işlev, kaç goroutine Doyu çağırırsa çağırsın, ömür boyu tam olarak bir kez koşuyor; sonraki her çağrı hiçbir şey yapmadan hemen dönüyor. sync.Oncein sıfır değeri de tıpkı kilit gibi kullanıma hazır: var bir sync.Once yazmak yeterli, ayrı bir kurulum gerekmiyor — çünkü Oncein içinde, ilk çağrının olup olmadığını izleyen bir sayaç ve o sayacı koruyan kendi kilidi zaten var. Oncein kendisi, bir önceki dersin ilkelini kullanarak kurulmuş bir araç.

Aynı bir değişkenine ait Do çağrılarına farklı işlevler verilse bile sonuç değişmiyor — koşan yalnız ilk çağrının işlevi, sonraki çağrılara verilen işlevler hiç çalışmadan atlanıyor. Bu örnekte otuz goroutine aynı işlevi verdiği için bu ayrım görünmüyor, ama sözleşme bunu zaten garanti ediyor: Oncein hafızası “hangi işlev verildi” değil, yalnız “bir kez çağrıldım mı” sorusuna verilen tek bitlik bir yanıt. Hangi goroutine’in kendi Do çağrısının gövdeyi gerçekten koşturduğu da ölçülmüyor ve ölçülemez — otuz goroutine aynı anda Doyu çağırıyor, hangisinin “kazandığı” zamanlamaya bağlı; ama hepsi aynı işlevi verdiği için bu belirsizlik, birinci dersin kilitsiz kurulumunda olduğu gibi, sonucu hiç etkilemiyor.

// main.go — bir kez calistirma, atomik sayac ile kilitli sayacin karsilastirilmasi, cift atomik degiskenin modellenmesi
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	fmt.Println("-- bir kez calistirma: N goroutine cagiriyor, govde bir kez kosuyor --")
	var bir sync.Once
	var kacKezKostu int64
	var ilkDeger int64
	const goroutineSayisi = 30
	var grup sync.WaitGroup
	grup.Add(goroutineSayisi)
	for i := 0; i < goroutineSayisi; i++ {
		go func() {
			defer grup.Done()
			bir.Do(func() {
				atomic.AddInt64(&kacKezKostu, 1)
				atomic.StoreInt64(&ilkDeger, 42)
			})
		}()
	}
	grup.Wait()
	fmt.Printf("govde kac kez kostu: %d\n", kacKezKostu)
	fmt.Printf("ilklendirilen deger: %d\n", ilkDeger)

	fmt.Println()
	fmt.Println("-- atomik sayac ile kilitli sayac ayni sonucu veriyor mu --")
	const artisSayisi = 500
	var atomikSayac int64
	var grup2 sync.WaitGroup
	grup2.Add(goroutineSayisi)
	for i := 0; i < goroutineSayisi; i++ {
		go func() {
			defer grup2.Done()
			for j := 0; j < artisSayisi; j++ {
				atomic.AddInt64(&atomikSayac, 1)
			}
		}()
	}
	grup2.Wait()

	var kilit sync.Mutex
	kilitliSayac := 0
	var grup3 sync.WaitGroup
	grup3.Add(goroutineSayisi)
	for i := 0; i < goroutineSayisi; i++ {
		go func() {
			defer grup3.Done()
			for j := 0; j < artisSayisi; j++ {
				kilit.Lock()
				kilitliSayac++
				kilit.Unlock()
			}
		}()
	}
	grup3.Wait()

	fmt.Printf("atomik sayac  : %d\n", atomikSayac)
	fmt.Printf("kilitli sayac : %d\n", kilitliSayac)
	fmt.Printf("ikisi ayni mi : %v\n", atomikSayac == int64(kilitliSayac))

	fmt.Println()
	fmt.Println("-- sinirlayici olcum: iki atomik degiskenin birlikte tutarliligi modelleniyor --")
	var bakiye, islemSayisi int64
	adim1 := make(chan struct{})
	bitti := make(chan [2]int64)

	go func() {
		<-adim1
		okunanBakiye := atomic.LoadInt64(&bakiye)
		okunanIslem := atomic.LoadInt64(&islemSayisi)
		bitti <- [2]int64{okunanBakiye, okunanIslem}
	}()

	atomic.AddInt64(&bakiye, 100)
	adim1 <- struct{}{}
	ara := <-bitti
	atomic.AddInt64(&islemSayisi, 1)

	fmt.Printf("yazan taraf tamamladiginda bakiye=%d islemSayisi=%d\n",
		atomic.LoadInt64(&bakiye), atomic.LoadInt64(&islemSayisi))
	fmt.Printf("okuyan tarafin arada gordugu   bakiye=%d islemSayisi=%d (tutarsiz cift)\n",
		ara[0], ara[1])
}
-- bir kez calistirma: N goroutine cagiriyor, govde bir kez kosuyor --
govde kac kez kostu: 1
ilklendirilen deger: 42

-- atomik sayac ile kilitli sayac ayni sonucu veriyor mu --
atomik sayac  : 15000
kilitli sayac : 15000
ikisi ayni mi : true

-- sinirlayici olcum: iki atomik degiskenin birlikte tutarliligi modelleniyor --
yazan taraf tamamladiginda bakiye=100 islemSayisi=1
okuyan tarafin arada gordugu   bakiye=100 islemSayisi=0 (tutarsiz cift)

PD9. Otuz goroutine aynı bir.Do(...) çağrısını neredeyse aynı anda yapıyor, ama kacKezKostu yalnız 1. sync.Oncein sözleşmesi burada bekleme grubundan farklı bir şekilde çalışıyor: bekleme grubu bir sayacı dışarıdan artırıp azaltan çağrılara dayanıyordu, Once ise kendi içinde bir kez çalıştıysa bir daha hiç çalışmayacağını kendisi kararlaştırıyor. Otuz çağrıdan yalnız biri gövdeyi gerçekten koşturuyor, geri kalan yirmi dokuzu Donun döndüğü anı — gövde daha önce koşmuşsa hemen, hâlâ koşuyorsa gövde bitene kadar — bekliyor. Bu da beklediği geldi sınıfının bir başka düzeneği: bekleyen goroutine’ler kendi başlarına durmuyor, ilk çağrının gövdeyi bitirmesi onları serbest bırakıyor.

ilkDegerin çıktısı bu garantinin ikinci yarısını gösteriyor: yalnız gövdenin bir kez koşması yetmiyor, gövdenin yazdığı değerin her goroutine tarafından görülebilmesi de gerekiyor. Otuz goroutine’den yalnız biri ilkDegere 42 yazıyor, ama Dodan dönen her goroutine — kendi çağrısı gövdeyi koşturmuş olsun ya da olmasın — aynı 42yi okuyor. Bu, sync.Oncein kendi içindeki senkronizasyonun bir sonucu: Dodan dönmek, yalnız “gövde bir yerde koştu” bilgisini değil, gövdenin yazdığı her değeri de beraberinde taşıyor.

PD10. İkinci ölçüm aynı toplamı iki ayrı yoldan üretiyor: atomikSayac, atomic.AddInt64 ile korunuyor ve hiçbir kilide dokunmuyor; kilitliSayac, önceki dersteki gibi Lock/Unlock çiftiyle korunuyor. Otuz goroutine, her biri beş yüz artışla, toplam on beş bin artış uyguluyor ve iki sayaç da tam olarak 15000e ulaşıyor. Bu eşitlik birinci dersteki kilitli/kilitsiz karşılaştırmasından farklı: orada kilitsiz kurulum paylaşılan tek bir belleğe hiç dokunmuyordu, burada atomikSayac gerçekten paylaşılan tek bir bellek konumu ve otuz goroutine ona gerçekten aynı anda yazıyor. Atomik işlem paylaşılan durum yarışını koşturmadan değil, güvenli kılarak dışlıyor — donanımın sağladığı bölünmez oku-değiştir-yaz işlemiyle, kilit gerekmeden.

M01/K05 İşletim Sistemi Kavramları‘nda kapanan atomik işlem kavramı burada aynen geçerli: bir işlemin bölünmez olması, araya başka hiçbir goroutine’in giremeyeceği anlamına geliyor. Go’nun sync/atomic paketi bu bölünmezliği belirli tamsayı ve işaretçi tipleri için donanım desteğiyle sağlıyor; AddInt64 çağrısının kendisi hiçbir zaman yarım kalmıyor, bir goroutine okuma ile yazma arasına asla giremiyor.

Bu güvence bir bedelle geliyor: sync/atomicin koruduğu şey yalnız int32, int64, uint32, uint64, işaretçi gibi belirli birkaç tip, ve her çağrı bu tiplerden tek birinin üzerinde çalışıyor. Kilit böyle bir sınır tanımıyordu — Lock/Unlock çifti arasına bir yapı, bir dilim, bir sözlük, ya da birden fazla değişkeni güncelleyen keyfi uzunlukta bir kod parçası girebiliyordu. Atomik işlemin dar sözleşmesi tam olarak bu esnekliğin bir kısmından vazgeçmenin karşılığı: tek bir tamsayıyı korumak için kilide hiç gerek kalmıyor, ama korunan şey bir tamsayının ötesine geçtiği anda atomik işlem yetersiz kalıyor.

sync/atomic bu derste kullanılan AddInt64 ve LoadInt64in yanında bir de karşılaştırıp değiştirme çağrısı taşıyor: verilen değişken beklenen değere eşitse yeni değeri yazıyor, değilse hiçbir şey yapmadan bunu bildiriyor — okuma ile yazma arasına hiçbir goroutine’in giremeyeceği bir başka bölünmez işlem. Bu ders o çağrıyı koşturmuyor; burada önemli olan sync/atomicin tek bir tamsayı üzerindeki her işlemi (ekleme, okuma, yazma, koşullu değiştirme) aynı bölünmezlik garantisiyle sunduğu, ama sunduğu şeyin her zaman tek bir değişkenle sınırlı kaldığı.

Sınırlayıcı Ölçüm: İki Atomik Değişkeni Birlikte Tutmak

PD11. Üçüncü kurulum tek bir değişken yerine iki tanesini — bakiye ve islemSayisi — kullanıyor ve ikisi de kendi başına atomik olarak okunup yazılıyor. Ama aralarında hiçbir birleşik koruma yok: yazan taraf önce bakiyeyi güncelliyor, sonra islemSayisini; bu iki güncelleme arasında kalan bir an, dışarıdan bakan bir goroutine’e tutarsız bir çift gösterebiliyor. Bu boşluk kanalla zorlanan bir sırayla modelleniyor: okuyan goroutine adim1 kanalından sinyal bekliyor, main önce bakiyeyi 100 yapıp sinyali gönderiyor, okuyan goroutine tam bu anda hem bakiyeyi hem islemSayisini okuyor — bakiye=100 ama islemSayisi henüz 0. main ancak bundan sonra islemSayisini artırıyor.

Bu, önceki dersteki kilit kapsamı modeliyle aynı ailede bir sınır, ama farklı bir yerde duruyor: orada sorun kilidin kapsamının yanlış çizilmesiydi, burada iki değişkenin hiç ortak bir kapsamı yok — her biri kendi başına atomik, ama ikisi birlikte değil. bakiye okunduğu an tanımlı bir değer taşıyor, islemSayisi okunduğu an da tanımlı bir değer taşıyor; okuma sonucu yanlış değil, yalnızca iki okumanın arasına başka bir goroutine’in bir yazması girebiliyor ve bu, tek başına doğru olan iki değeri bir arada yanlış bir hikâyeye dönüştürebiliyor. Atomik işlem tek bir değişkeni korumak için var; birden çok değişkenin birlikte tutarlı kalması gerekiyorsa atomik işlem yetmiyor, o korumayı yeniden bir kilit üstleniyor.

Bu sınırın kendisi bir tasarım hatası değil, önceki dersteki kilit kapsamı örneğinin bir genellemesi. Orada sorun tek bir sayacın oku-hesapla-yaz üçlüsünün kilidin dışına taşmasıydı; burada iki ayrı sayacın ikisi birden tek bir kapsam altında değil. İkisi de aynı soruyu soruyor: korunması gereken işlem nerede başlayıp nerede bitiyor? bakiye ve islemSayisi ayrı ayrı atomik olduğu için bu soru kaynağa bakılarak fark edilmiyor — her iki değişken de kendi satırında doğru görünüyor, aralarındaki ilişkinin korunmadığı yalnızca iki değişkeni birlikte okumaya çalışan bir kod parçası yazılıp koşturulduğunda ortaya çıkıyor.

Kilit, Bir Kez Çalıştırma, Atomik İşlem

Üç ders boyunca ölçülen üç araç aynı soruya üç ayrı dar yanıt veriyor. Kilit “hangi kod parçası aynı anda tek bir goroutine tarafından çalışsın?” sorusuna yanıt veriyor ve bu kapsamı Lock/Unlock çiftiyle kendi çiziyor — çizim yanlış olursa (birinci dersin sınırlayıcı ölçümü) kilit bunu fark etmiyor. Bir kez çalıştırma “bu gövde daha önce koştu mu?” sorusuna yanıt veriyor ve bu soruyu sonsuza dek hatırlıyor; ikinci bir Do çağrısı gövdeyi bir daha hiç çalıştırmıyor, çünkü soru zaten bir kez yanıtlandı. Atomik işlem “bu tek değişkene aynı anda kaç goroutine dokunabilir?” sorusuna yanıt veriyor ve yanıtı donanım düzeyinde garanti ediyor, ama soruyu yalnız tek bir değişken için soruyor.

Üçü de paylaşılan durumu koruyor, ama üçünün kapsamı birbirinin yerine geçmiyor: bir kez çalıştırmayla bir sayacı sürekli korumak, ya da atomik işlemle birden fazla adımı tek bir işlem gibi göstermek mümkün değil. Kod tabanında hangi ilkelin seçildiği, korunması gereken şeyin doğasını — bir olay mı, tek bir değer mi, yoksa keyfi bir kod parçası mı — açığa çıkarıyor. Bu üç dersin ortak sonucu bu: paylaşılan durumu korumanın tek bir doğru yolu yok, korunacak şeyin şekline göre değişen üç ayrı dar sözleşme var.

Bu üç dar sözleşme aynı zamanda birbirine iç içe geçebiliyor. sync.Oncein kendisi bir kilitle kuruldu; ikinci dersin KorumaliSayacı gibi bir tip, kendi kilidini taşırken aynı zamanda bir sync.Once alanı taşıyıp yalnız ilk erişimde bir alanı ilklendirebilir. Atomik işlem de kilidin yerini almıyor, kilidin bir yerine geçebileceği en dar durumları kilitsiz bırakıyor. Üçü de aynı ölçü ekseninin birer görünümü: bir goroutine’in paylaşılan bir duruma nasıl eriştiği, o durumun ne kadarını, ne kadar süreyle koruma altında tuttuğuyla ölçülüyor.

Özet

  • sync.Once, Doya verilen gövdenin kaç goroutine çağırırsa çağırsın tam olarak bir kez koşmasını garanti ediyor; otuz çağrıdan yalnız biri gövdeyi koşturdu.
  • Bekleyen goroutine’ler Oncein gövdesi bitene kadar bloklanıyor — beklediği geldi sınıfının kilit ve bekleme grubundan farklı bir düzeneği.
  • sync/atomic, tek bir değişken üzerindeki işlemleri donanımın bölünmez oku-değiştir-yaz desteğiyle koruyor; otuz goroutine on beş bin artış uyguladı ve atomik sayaç kilitli sayaçla aynı sonucu (15000) verdi.
  • İki ayrı atomik değişken, her biri kendi başına tutarlı olsa da, birlikte okunduğunda tutarsız bir çift verebiliyor; bu, kanalla zorlanmış bir sırayla modellendi, koşturulmadı.
  • Atomik işlem tek bir değişkeni korur; birden fazla değişkenin birlikte tutarlılığı gerekiyorsa kilit kapsamı hâlâ gereken araç.
  • Üç ders boyunca ölçülen kilit, bekleme grubu, bir kez çalıştırma ve atomik işlem aynı ölçü ekseninin dört ayrı görünümü: her biri paylaşılan durumun ne kadarını, ne kadar süreyle koruma altında tuttuğuyla tanımlanıyor.

Sonraki Adım

Üç ders boyunca paylaşılan durum kilitle, bekleme grubuyla ve şimdi bir kez çalıştırma ve atomik işlemlerle korundu — ama her ölçüm korumanın var olduğunu baştan biliyordu, çünkü kaynağı biz yazdık. Bu üç dersin hiçbirinde gerçek bir yarış koşulu koşturulmadı: birinci ders bunu ayrık bellekle ve zorlanmış bir modelle, bu ders de zorlanmış bir modelle dışladı. Gerçek bir kaynak tabanında korumanın unutulup unutulmadığını, kaynağı okuyarak değil, kim söylüyor? Kursun son dersi bu soruyu, dilin kendi dağıtımıyla gelen yarış koşulu saptayıcısına soruyor: korumasız bir paylaşım, kilitli bir paylaşım ve kanal üzerinden bir aktarım aynı saptayıcıdan geçiriliyor, saptayıcının verdiği yanıt (buldu mu, bulamadı mı) okunuyor, ve saptayıcının kendi sınırı — hiç çalışmayan bir yoldaki yarışı görememesi — kursun son ölçümü olarak ortaya çıkıyor.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat