İçeriğe geç
academia.sh

Ders 04 / 14

select Deyimi

Birden çok kanal üzerinde aynı anda beklemenin karşılığı select deyimidir: yalnız bir dal hazırsa o dal belirlenimci biçimde seçilir, hiçbiri hazır değilse ve bir default dalı varsa o dal seçilir. Yazım sırası bir öncelik taşımaz. Sınırlayıcı ölçüm: iki dal aynı anda hazırsa seçim rastgeledir ve bu durum koşturulmuyor, yalnızca modelleniyor — rastgelelik dilin kendi kararı.

İçindekiler

Önceki ders bir kanalın kapasitesinin engellemeyi nerede başlattığını ölçtü ve select ile default’u yalnız tek bir kanalı yoklamak için kullandı. Ama bir goroutine çoğu zaman birden çok kaynaktan aynı anda haber bekler: birkaç sonuç kanalından biri hazır olabilir, bir iptal sinyali gelebilir, bir zaman aşımı dolabilir. select deyimi bunun için var — birden çok kanal üzerinde aynı anda bekleyip hangisi önce hazır olursa onu işleyen tek yapı.

Bu ihtiyaç, tek bir kanalı beklemenin yetmediği her yerde ortaya çıkıyor. Bir goroutine yalnız bir kanalı beklerken <-kanal yazmak yeterlidir; ama iki ayrı kaynaktan hangisi önce gelirse onu işlemek isteyen bir goroutine, bunu tek bir alım ifadesiyle yazamaz. select tam olarak bu boşluğu dolduruyor.

Bir select bloğu, her biri bir kanal işlemiyle başlayan birden çok case içerir. Deyim bu işlemlerden hazır olanlardan birini seçip o dalı çalıştırır; hiçbiri hazır değilse ve bir default dalı varsa hemen o çalışır, yoksa select en az bir dal hazır olana kadar bekler. Bu ders bu seçimin nasıl belirlendiğini, nerede belirlenimci nerede rastgele olduğunu ölçüyor.

selectin her dalı bir gönderim ya da bir alım olabilir; aynı select içinde bazı dallar gönderim, bazıları alım olabilir, önemli olan yalnızca hangisinin o an engellemeden tamamlanabileceğidir. Bu, selecti bir switchten ayıran ilk fark: switch bir değeri sırayla dallarla karşılaştırırken, select her dalın kendi kanal işlemini aynı anda dener ve hangisi hazırsa ona göre karar verir. Bir kanal işlemi olmayan bir case yazmak derlenmez; selectin dalları her zaman bir gönderim ya da alımla başlar.

Bir selectin kaç dalı olabileceğine dilde bir üst sınır yok; bir select bloğu iki kanalı da, on kanalı da bekleyebilir. Bu ders yalnızca iki kanallı örnekler kullanıyor, çünkü ölçülen kurallar — belirlenimli seçim, sıra bağımsızlığı, rastgelelik — dal sayısından bağımsız; iki dal bu kuralları göstermek için yeterli ve okunması en kolay durum. Dal sayısı arttıkça ölçülen kurallar değişmiyor, yalnızca aynı anda hazır olabilecek dal sayısı artıyor; üçüncü bölümdeki rastgelelik kuralı da ikiden fazla dal hazır olduğunda aynı biçimde geçerli kalıyor.

Tek Dal Hazırken Seçim Belirlenimlidir

Bir select içindeki dallardan yalnız biri hazırsa, select başka hiçbir seçenek düşünmeden o dalı çalıştırır. Hiçbiri hazır değilse ve bir default dalı varsa, select hiç beklemeden o dala düşer — önceki derste kapasiteyi yoklamak için kullanılan tam olarak bu davranıştı.

// tekilVeVarsayilan.go — yalniz bir dal hazirken secim belirlenimli, hicbiri hazir degilken varsayilan dal secilir
package main

import "fmt"

func sec(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	default:
		return "varsayilan: hicbiri hazir degil"
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)

	fmt.Println("hicbiri hazir degilken:", sec(a, b))

	a <- "ilk deger"
	fmt.Println("yalniz a hazirken     :", sec(a, b))

	b <- "ikinci deger"
	fmt.Println("yalniz b hazirken     :", sec(a, b))
}
hicbiri hazir degilken: varsayilan: hicbiri hazir degil
yalniz a hazirken     : a: ilk deger
yalniz b hazirken     : b: ikinci deger

GK14. — Üç çağrı da aynı sec fonksiyonunu, aynı kaynak koduyla çağırıyor; değişen tek şey kanalların doluluğu. İlk çağrıda ikisi de boş, default seçiliyor. aya bir değer konduğunda ikinci çağrı a dalını seçiyor ve bu arada adaki değeri tüketiyora şimdi yine boş. Üçüncü çağrıda yalnız b doluyken b dalı seçiliyor. Hiçbir çağrıda birden fazla dal aynı anda hazır olmadı; bu yüzden her üç sonuç da kesinlikle tekrarlanabilir.

Bu ölçümün tek goroutine içinde, hiç eşzamanlılık gerektirmeden yapılabilmesi önemli: a ve b kanallarına doğrudan ana goroutine’den değer konuluyor, hiçbir goroutine başlatılmıyor. Bu, selectin kendisinin eşzamanlılığa özgü bir yapı olmadığını gösteriyor — select yalnız birden çok kanal işlemi arasında seçim yapan bir dildir; kanalların karşı tarafında goroutine olup olmaması selectin kendi davranışını değiştirmiyor. Sonraki derslerde select hep goroutine’ler arasında kullanılacak, ama bu ders onun çekirdek davranışını en yalın hâliyle, tek goroutine’de gösteriyor.

Bu ayrım küçük görünse de önemli: selectin belirlenimli davranışı zamanlamadan değil, yalnızca hangi kanalların o an dolu olduğundan kaynaklanıyor. Aynı kod, kanalları farklı bir sırayla doldursaydı ya da farklı bir makinede çalıştırılsaydı sonuç yine aynı olurdu — çünkü belirleyici olan zamanlama değil, doluluk durumunun kendisi.

Yazım Sırası Bir Öncelik Değildir

selectin dalları yukarıdan aşağı bir öncelik sırası taşımaz — bu, sıradan bir if/else if zincirinden ya da bir switchten temel bir farktır. Hazır olan tek dal hangi sırada yazılmış olursa olsun seçilir; yazım sırası yalnızca okunabilirlik içindir, çalışma zamanı davranışını etkilemez.

// siraOnemsiz.go — select dallarinin yazim sirasi hangi dalin secilecegini belirlemez
package main

import "fmt"

func sec(a, b chan string) string {
	select {
	case v := <-b:
		return "b: " + v
	case v := <-a:
		return "a: " + v
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)
	a <- "sadece a doldu"

	fmt.Println(sec(a, b))
}
a: sadece a doldu

GK15. — Bu fonksiyonda b dalı kaynakta önce, a dalı sonra yazılı. Yalnız a doluyken çalıştırıldığında sonuç yine a: sadece a doldu — kaynaktaki sıralamanın sonucu değiştirmediğini gösteriyor. selectin her casei eşit ağırlıkta değerlendirilir; hazır olan tek dal neyse o çalışır. Bu, bir sonraki bölümün sorusunu doğuruyor: iki dal aynı anda hazır olsaydı ne olurdu, madem sıralama bir öncelik vermiyor?

Bu davranış, selectin dallarını değerlendirme biçimiyle ilgili. Kaynakta yazılan sıra yalnızca derleyicinin caseleri okuma sırasıdır; çalışma zamanında select, çalıştırılacağı anda dalların tümünü birden değerlendirip hazır olanları bulur, sonra bunlar arasından seçer. Sıradan bir if a != nil { ... } else if b != nil { ... } zincirinde ilk koşulu sağlayan dal kazanırdı — orada yazım sırası gerçekten bir önceliktir. select bu modele uymuyor; bu fark, selecti öğrenirken en sık karışan noktalardan biri, çünkü sözdizimi switche çok benziyor ama değerlendirme kuralı farklı.

Sınırlayıcı Ölçüm: İki Dal Birden Hazırsa Seçim Rastgeledir

Buraya kadarki her ölçüm, herhangi bir anda en fazla bir dalın hazır olduğu durumları kurdu. İki dal aynı anda hazır olduğunda dil hangi kuralı uygular? Go’nun kendi belirtimi bu soruya açık bir yanıt veriyor: birden çok dal aynı anda hazırsa, select aralarından tekdüze rastgele birini seçer. Bu, kursun bu derse kadarki her ölçümünün dayandığı belirlenimcilik kuralının bilinçli bir istisnası — ve bu yüzden bu ders bu durumu koşturmuyor, yalnızca modelliyor.

// modellenir.go — iki dal ayni anda hazirsa secim rastgeledir; bu ders bu dali kosturmaz, yalniz gosterir
package main

func ikiDalBirdenHazirsaSecim(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	}
}

GK16. — Bu fonksiyon, a ve bnin ikisi de dolu çağrıldığında selectin tam olarak karşılaştığı durumu gösteriyor: derleyici burada iki dal arasında sabit bir tercih yapmıyor, çalışma zamanı her çağrıda yeniden rastgele seçiyor. ikiDalBirdenHazirsaSecim bu dersin hiçbir yerinde çağrılmıyor — çağrılsaydı, iki koşum arasında farklı sonuçlar üretebilirdi ve bu, kod_dogrula’nın aradığı “her koşumda aynı çıktı” koşulunu bilerek bozardı. Fonksiyon yalnızca var olduğu için derleniyor; kaynağın kendisi rastgeleliğin nerede ortaya çıktığını gösteriyor, koşumu değil.

Bu rastgelelik bir eksiklik değil, bilinçli bir tasarım kararı. Eğer select iki hazır dal arasında hep kaynaktaki ilk dalı seçseydi, programcılar zamanla bu sırayı bir öncelik kuralı gibi kullanmaya başlar ve kanallardan birinin sistematik olarak açlığa düşmesine yol açabilirdi — art arda aynı iki dal sürekli hazır kaldığında, ikinci dal hiç çalışmayabilirdi. Rastgele seçim bu riski dilin kendisi ortadan kaldırıyor: hiçbir dal, yalnızca kaynakta önce yazıldığı için sistematik olarak öne geçemiyor.

Bu ölçülemezlik, önceki bölümdeki “yazım sırası bir öncelik değildir” bulgusunun asıl gerekçesi. Sıra bir öncelik taşısaydı, iki dal aynı anda hazır olduğunda hangisinin seçileceği yine belirlenimci olurdu — ilk yazılan kazanırdı. Sıranın hiçbir ağırlığı olmadığı için, iki dal birden hazır olduğunda gerçekten seçilecek bir kural kalmıyor; dil bu boşluğu rastgelelikle dolduruyor, M09/K01 Go Temelleri kursunda görülen “tanımlı” sınıfından farklı bir yol: orada yazılmayan bir şeyin yerine sabit bir değer konuyordu, burada yazılmayan bir şeyin yerine her seferinde yeniden çekilen bir değer konuyor. Bu ders bu ayrımı bir eksen olarak sürdürmüyor, yalnız bir kez not ediyor.

Bu ders ikiDalBirdenHazirsaSecimi hiç çağırmadığı için, iki koşum arasındaki farkı göstermenin tek yolu kaynağı okumak. Bu, kursun M09/K03’e kadarki bütün derslerinin aksine, “aynı kaynak farklı koşumlarda farklı sonuç verebilir” iddiasının koşturmadan kanıtlandığı tek yer: rastgeleliğin varlığını Go’nun kendi belirtimi garanti ediyor, bu ders yalnız o garantinin nerede devreye girdiğini kaynak üzerinde gösteriyor.

select Bloklamayan Bir Denetim Aracıdır

default dalı selecti yalnızca bir yoklama aracına değil, akışı hiç durdurmadan devam ettiren bir denetim aracına dönüştürüyor. Bu, bir kanaldaki bütün hazır değerleri, hiçbiri kalmayana kadar tüketmek için kullanılabilir — kanal boşaldığı anda döngü, beklemeden kendini durdurur.

Bu kullanım, selectin iki farklı işlevini birbirinden ayırıyor. Birinci bölümdeki kullanımda select birden çok kanal arasında seçim yapıyordu — hangi kaynağın hazır olduğunu belirliyordu. Burada select tek bir kanal üzerinde, o kanalın hazır olup olmadığını sormak için kullanılıyor; default dalının varlığı, “hazır değilse bekleme” demenin dildeki karşılığı. İki kullanım da aynı sözdizimini paylaşıyor, ama amaçları farklı: biri birden çok kaynak arasında karar veriyor, öteki tek bir kaynağın anlık durumunu okuyor.

// engellenmeyenDenetim.go — select, hazir olmayan bir kanali beklemeden akisi surdurmek icin kullanilir
package main

import "fmt"

func hazirsaAl(kanal chan int) (int, bool) {
	select {
	case v := <-kanal:
		return v, true
	default:
		return 0, false
	}
}

func main() {
	sonuclar := make(chan int, 3)
	sonuclar <- 10
	sonuclar <- 20

	toplam, adet := 0, 0
	for {
		v, tamam := hazirsaAl(sonuclar)
		if !tamam {
			break
		}
		toplam += v
		adet++
	}
	fmt.Println("kanaldan engellenmeden alinan deger sayisi:", adet, "toplam:", toplam)

	_, tamam := hazirsaAl(sonuclar)
	fmt.Println("kanal bosalinca uzerine deneme            :", tamam)
}
kanaldan engellenmeden alinan deger sayisi: 2 toplam: 30
kanal bosalinca uzerine deneme            : false

GK17.sonuclar kanalına yalnızca iki değer konuldu; döngü bu ikisini hazirsaAl ile tüketiyor ve üçüncü denemede tamam false döndüğü için döngü kendiliğinden duruyor — hiçbir goroutine beklenmedi, hiçbir sıra çizelgeleyiciye bırakılmadı. Bu düzenek, bir kanalın “şu an elimde ne var” diye sorulmasını sağlıyor; kanalın kendisi boşsa, soran taraf beklemeden yoluna devam edebiliyor. Bu, önceki derste tampon kapasitesini yoklamak için kullanılan tekniğin genel biçimi: orada tek bir kanalın dolu olup olmadığı soruluyordu, burada aynı teknik bir kanalı tüketmek için kullanılıyor.

Döngünün durma koşulu da dikkat çekici: for döngüsü sabit bir sayıya göre değil, hazirsaAlin döndürdüğü ikinci değere göre duruyor. Kaynak koddan kaç kez döneceği okunamaz — sonuclar kanalına kaç değer konulmuş olursa olsun aynı döngü çalışır ve tam olarak o kadar adımda durur. Bu, range ile bir dilim gezmekten farklı: rangein kaç kez döneceği dilimin uzunluğundan önceden okunabilirken, bu döngünün kaç kez döneceği yalnızca koşarak öğrenilir. Bu fark, kapanış dersinin ölçtüğü range over channel davranışıyla karşılaştırıldığında daha da netleşiyor: range bir kanalın kapanmasını bir durma koşulu sayarken, buradaki döngü kanalın o anki doluluğunu bir durma koşulu sayıyor — ikisi de sonlu bir döngü üretir, ama farklı bir sinyale göre.

Bu kullanım biçiminin bir bedeli var: hazirsaAl bir goroutine’in üretmesini beklemez, yalnızca o anda hazır olanı alır. Kanala henüz hiçbir şey yazılmamışsa, hazirsaAl her zaman false döner — üretici goroutine bir mikrosaniye sonra bir değer gönderecek olsa bile. Bloklamayan bir denetim aracı, karşılığında beklemeyi de kaybeder; hangisinin gerektiği, ne zaman selectin beklemesine izin verileceği, ne zaman hemen devam etmesi gerektiğine bağlı bir tasarım kararı.

Bu tehlike, default dalını bir döngü içine koymanın yanlış kullanımıyla daha da belirginleşiyor: eğer amaç bir değerin gelmesini beklemekse ve döngü default dalına her düştüğünde hemen tekrar denerse, program hiçbir zaman bloklanmadan, işlemciyi sürekli meşgul ederek bekler — bu, kilit ve kritik bölge bağlamında M01/K05 İşletim Sistemi Kavramları kursunda kurulmuş meşgul bekleme kalıbının select üzerindeki karşılığı. Bu ders o kalıbı kurmuyor: hazirsaAl bir kez çağrılıyor, bir döngüde beklenmiyor. Beklemek gerektiğinde doğru araç defaultsuz bir selecttir — o zaman select kendisi hazır bir dal olana kadar bloklanır ve işlemciyi meşgul etmez; bu, birinci bölümdeki sec fonksiyonunun default dalı olmadan yazılmış hâli olurdu.

Özet

  • select, birden çok kanal işleminden yalnızca bir tek dalın hazır olduğu durumda o dalı belirlenimci biçimde seçer.
  • Dalların kaynaktaki yazım sırası bir öncelik taşımaz; hazır olan dal hangi sırada yazılmış olursa olsun seçilir.
  • Hiçbir dal hazır değilken bir default dalı varsa hemen çalışır; select bu şekilde hiç beklemeyen bir yoklama ve denetim aracına dönüşür.
  • İki dal aynı anda hazır olduğunda seçim tekdüze rastgeledir; bu kursun ölçtüğü belirlenimcilik kuralının bilinçli istisnasıdır ve bu yüzden koşturulmaz, yalnız modellenir.
  • select+default bir kanalı, hiçbir goroutine beklemeden, elindeki değerler bitene kadar tüketmek için kullanılabilir.
  • defaultsuz bir select hiçbir dal hazır olmayınca bloklanır ve işlemciyi meşgul etmez; beklemek gerektiğinde doğru araç budur, defaultlı bir döngü değil.

Sonraki Adım

Bu derste kapanan bir kanalın select içindeki davranışına hiç değinilmedi. Kapanmış bir kanaldan alım da tıpkı dolu bir kanaldan alım gibi bir select dalını hemen hazır kılar — ama gelen yanıt, ölçülen değerin kendisi değil, kanalın artık boş kalacağının haberi. Sıradaki ders bu son davranışı ölçüyor: bir kanalın kapatılması bir sonlanma sinyalidir, ve bu sinyal hem range hem de doğrudan alımda kendini nasıl gösteriyor.

İ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