Ders 01 / 14
Goroutine
On iki goroutine'in yalnız 2'si kendi durdu; 5'i başka bir goroutine'in yaptığı bir işi bekleyip geldiği için, 2'si bir bağlamın iptal edilmesiyle, 3'ü hiç durmadan: kursun bağlayıcı dağılımı bu derste üretiliyor. Bir goroutine başlatmak tek satırdır ve o satır ne zaman duracağını hiç söylemez — durma kararı çoğunlukla goroutine'in kendi gövdesinde değil, başka birinin yaptığı bir işte duruyor. Sınırlayıcı ölçüm: kendi durdu sınıfı da bir sözleşmedir, çünkü gövdesi biten bir goroutine bunu kimseye kendiliğinden bildirmiyor.
İçindekiler
Arayüzler, Hatalar ve Genellikler kursu aynı işin arayüzle mi genellikle mi yazıldığına göre
bir bağın nerede kurulduğunu ölçerek kapandı: on altı bağın sekizi bildirimde ya da kullanım
yerinde değil, çalışma zamanında değerin içinde taşınıyordu. Bu ders aynı soruyu zamana
taşıyor. go sözcüğü bir fonksiyon çağrısının önüne konduğunda yeni bir eşzamanlı yürütme
birimi başlar ve çağıran satır hiç beklemeden devam eder — bedeli yok gibi görünen tek
satırlık bir karar. Ama o satır bir şeyi hiç söylemez: başlattığı goroutine’in ne zaman ve
nasıl duracağını.
Kaynağa bakan biri go sözcüğünü görür, durma koşulunu göremez. Durma kararı çoğu zaman
goroutine’in kendi gövdesinde bile yazılı değildir; başka bir goroutine’in yaptığı bir işe —
bir kanala gönderilen değer, bırakılan bir kilit, iptal edilen bir bağlam — bağlıdır. Bu
kursun tek sorusu budur: bu goroutine nasıl duracak?
Başlatmakla Durmak Ayrı İki İştir
Bir goroutine başlatmak, aynı fonksiyonu doğrudan çağırmaktan tek bir sözcük farklıdır:
f() çağıran goroutine’de çalışır ve dönene kadar çağıranı bloklar; go f() yeni bir
goroutine’de çalışır ve çağıran satır hemen bir sonraki satıra geçer. Bu fark, go
sözcüğünün bütün sözüdür. Başlatılan goroutine’in ne zaman biteceği, bitip bitmeyeceği,
bitişinin çağırana nasıl ulaşacağı — bunların hiçbiri go sözcüğünün taahhüdünde değildir.
Derleyici bu boşluğu hiç sormaz: bir goroutine’in çağırdığı fonksiyonun ne zaman döneceği,
dönüp dönmeyeceği, go sözcüğünün derlenmesi için hiçbir koşul değildir. Bir goroutine
sonsuza kadar çalışabilir, hiç çalışmadan takılabilir, ya da bir mikrosaniyede biter —
derleyici üçünü de aynı tek satırla kabul eder. Bu, kursun M09/K01 dersinde görülen
“reddedildi” sınıfından tamamen farklı bir durum: orada derleyici yazılmayan bir bağı
kabul etmiyordu, burada yazılmayan bir bağı hiç sorgulamıyor bile.
Bu boşluk dört farklı biçimde doluyor, ve doldurma işini goroutine’in kendisi değil, çoğu zaman başka biri yapıyor:
- goroutine kendi gövdesini bitirip kendiliğinden duruyor (kendi);
- bir kanal, kilit ya da bekleme grubu üzerinde bekliyor ve başka bir goroutine’in attığı bir adım onu serbest bırakıyor (beklediği geldi);
- beklediği şey hiç gelmiyor ama bir bağlam iptal edildiği için duruyor (iptal);
- ya da hiçbiri olmuyor ve goroutine hiç durmuyor.
Bu dört sınıf bu dersin ve bütün kursun ölçü eksenidir. Go’nun eşzamanlı yürütme birimine
verdiği ad goroutine‘dir ve bu ad çevrilmeden kullanılır; goroutine işletim
sisteminin zamanladığı bir iş parçacığıyla aynı şey değildir, dilin kendi çalışma zamanı
binlerce goroutine’i az sayıda gerçek iş parçacığı üzerinde kendi zamanlayıcısıyla
yürütür. Bu ayrımın kendisi bu dersin konusu değil; iki düzenek burada karşılaştırılmıyor,
yalnızca goroutine’in kendi başına bir kavram olduğu bir kez kayda geçiriliyor.
Durma Sebebini Saydıran Bir Çerçeve
Dört sınıfı ölçülebilir kılmanın yolu, her goroutine’in kendi durma sebebini bir kanala
yazmasını istemek ve raporları toplayıp sıralı basmaktır — goroutine’ler sıra vermez, bu
yüzden sıra ölçümün kendisinde kurulur. Defter tipi bunu yapıyor: bildir her raporu bir
kanala gönderiyor, topla tam olarak beklenen sayıda raporu okuyup ada göre sıralıyor.
// dur.go — bir goroutine nasil duruyor: kendi mi, bekledigi geldigi icin mi, iptal edildigi icin mi, hic mi
package main
import (
"fmt"
"runtime"
"sort"
)
var kendi, geldi, iptal, hic int
type rapor struct{ ad, sebep string }
// Defter raporlari toplar ve sirali basar; goroutine'ler sira vermez.
type Defter struct {
kanal chan rapor
taban int
}
func yeniDefter(kapasite int) *Defter {
return &Defter{kanal: make(chan rapor, kapasite), taban: runtime.NumGoroutine()}
}
func (d *Defter) bildir(ad, sebep string) { d.kanal <- rapor{ad, sebep} }
func (d *Defter) topla(beklenen int) []rapor {
r := make([]rapor, 0, beklenen)
for i := 0; i < beklenen; i++ {
r = append(r, <-d.kanal)
}
sort.Slice(r, func(i, j int) bool { return r[i].ad < r[j].ad })
return r
}
func gozle(ad, sebep string) {
switch sebep {
case "kendi":
kendi++
case "geldi":
geldi++
case "iptal":
iptal++
default:
hic++
}
fmt.Printf("%-38s %s\n", ad, sebep)
}
func (d *Defter) sizintiVarMi() bool { return runtime.NumGoroutine() > d.taban }
func ozet() {
fmt.Printf("durma: %d kendi, %d bekledigi geldi, %d iptal, %d hic durmadi\n",
kendi, geldi, iptal, hic)
}
GK1. — bildir yalnız bir kanala yazıyor, hiçbir goroutine kendi başına ekrana
yazmıyor; bütün fmt.Printf çağrıları gozle içinde ve yalnız ana goroutine tarafından
çalıştırılıyor. Bu ayrım rastgele değil: birden çok goroutine aynı anda ekrana yazsaydı,
satırların hangi sırayla çıkacağı da bir sıra vermezdi ve ölçüm kendi kendini bozardı.
taban alanı sizintiVarMinin neye göre karşılaştırma yapacağını, ölçüm başlamadan önceki
goroutine sayısını tutuyor.
Sıranın toplanıp sıralanması bir zevk meselesi değil, bu kursun bütün derslerinde geçerli
bir zorunluluktur: goroutine’lerin hangi sırayla bittiği çalışma zamanının zamanlama
kararına bağlıdır ve bu karar koşumdan koşuma değişebilir. Aynı sonucu her koşumda almanın
tek yolu, sırayı ya bir kanal/kilit/bekleme grubuyla zorlamak ya da sonuçları toplayıp
kendi kriterimize göre sıralamaktır; burada seçilen kriter ad alanının alfabetik sırası.
Sabit bir bekleme süresi koyup “muhtemelen bu kadar sürede biter” varsaymak bu iki yoldan
hiçbirine girmez — ortam yüküne göre bazen yeter, bazen yetmez, ve bu derste hiç
kullanılmıyor.
On İki Goroutine, Dört Durma Biçimi
Ölçüm aynı pakette bu çerçeveyi kullanıyor ve on iki goroutine’i dört sınıfa dağıtıyor.
İlk ikisi kendi gövdesini bitirip duruyor. Sonraki beşi beş ayrı düzenekle bekliyor —
tamponsuz bir kanaldan alım, kapanmış bir kanalda range, bir kilidin bırakılması, bir
bekleme grubunun sıfırlanması, birden çok kanal üzerinde select — ve beşi de başka bir
goroutine’in (ya da ana goroutine’in) attığı bir adımla serbest kalıyor. İki tanesi bir
bağlamın iptaliyle duruyor. Son üçü hiç durmuyor: alıcısı olmayan bir kanala gönderim
yapmaya çalışıyor, kapanmayan bir kanalda range yapıyor, hiç Done çağrılmayan bir
bekleme grubunu bekliyor.
// olcum.go — on iki goroutine, dort durma bicimi
package main
import (
"context"
"fmt"
"sync"
)
func main() {
d := yeniDefter(32)
go func() { d.bildir("01 govdesi biten goroutine", "kendi") }()
go func() {
t := 0
for i := 0; i < 3; i++ {
t += i
}
_ = t
d.bildir("02 dongusu biten goroutine", "kendi")
}()
kanal := make(chan int)
go func() {
<-kanal
d.bildir("03 tamponsuz alim", "geldi")
}()
kanal <- 1
kapanan := make(chan int, 2)
kapanan <- 7
close(kapanan)
go func() {
for range kapanan {
}
d.bildir("04 kapanan kanalda aralik", "geldi")
}()
var kilit sync.Mutex
kilit.Lock()
go func() {
kilit.Lock()
kilit.Unlock()
d.bildir("05 kilidi bekleyen", "geldi")
}()
kilit.Unlock()
var grup sync.WaitGroup
grup.Add(1)
go func() {
grup.Wait()
d.bildir("06 bekleme grubunu bekleyen", "geldi")
}()
grup.Done()
secim := make(chan int, 1)
digeri := make(chan int)
secim <- 5
go func() {
select {
case <-secim:
d.bildir("07 select ilk hazir daldan", "geldi")
case <-digeri:
d.bildir("07 select ikinci daldan", "geldi")
}
}()
ctx, durdur := context.WithCancel(context.Background())
go func() {
<-ctx.Done()
d.bildir("08 baglam iptali bekleyen", "iptal")
}()
go func() {
for {
select {
case <-ctx.Done():
d.bildir("09 dongude baglam denetleyen", "iptal")
return
default:
}
}
}()
durdur()
terkedilmis := make(chan int)
go func() {
terkedilmis <- 1
d.bildir("10 alicisiz gonderim", "hic")
}()
kapanmayan := make(chan int)
go func() {
for range kapanmayan {
}
d.bildir("11 kapanmayan kanalda aralik", "hic")
}()
var bitmeyen sync.WaitGroup
bitmeyen.Add(1)
go func() {
bitmeyen.Wait()
d.bildir("12 hic Done almayan bekleme", "hic")
}()
fmt.Println("-- goroutine nasil durdu --")
for _, r := range d.topla(9) {
gozle(r.ad, r.sebep)
}
gozle("10 alicisiz gonderim", "hic")
gozle("11 kapanmayan kanalda aralik", "hic")
gozle("12 hic Done almayan bekleme", "hic")
fmt.Println("gonderim simdi gecer mi :", denenirMi(terkedilmis))
fmt.Println("durmayan goroutine kaldi mi :", d.sizintiVarMi())
ozet()
}
func denenirMi(k chan int) bool {
select {
case k <- 1:
return true
default:
return false
}
}
-- goroutine nasil durdu -- 01 govdesi biten goroutine kendi 02 dongusu biten goroutine kendi 03 tamponsuz alim geldi 04 kapanan kanalda aralik geldi 05 kilidi bekleyen geldi 06 bekleme grubunu bekleyen geldi 07 select ilk hazir daldan geldi 08 baglam iptali bekleyen iptal 09 dongude baglam denetleyen iptal 10 alicisiz gonderim hic 11 kapanmayan kanalda aralik hic 12 hic Done almayan bekleme hic gonderim simdi gecer mi : false durmayan goroutine kaldi mi : true durma: 2 kendi, 5 bekledigi geldi, 2 iptal, 3 hic durmadi
GK2. — İlk dokuz satır d.topla(9) çağrısından geliyor ve ade göre sıralanmış
çıkıyor; son üçü ölçümün kendisi gozle ile elle basıyor, çünkü bu üçü hiç rapor
göndermiyor ve topla onları hiç bekleyemez. Dokuz sayısı bu derste veriden değil
kurgudan geliyor — kaç goroutine’in rapor göndereceğini yazar baştan biliyor, ölçüm
bunu doğrulamıyor; bu, üçüncü bölümdeki sınırlayıcı ölçümün konusu.
On iki goroutine’in 2’si kendi durdu, 5’i beklediği geldiği için, 2’si iptal edildiği
için, 3’ü hiç durmadı. Bu dağılım kursun geri kalanı için bağlayıcı; sonraki dersler
kendi diliminin satırlarını basıp bu sayıya tek cümleyle atıf yapacak. GOMAXPROCS=1 dahil
her koşumda aynı çıktı üretiliyor, çünkü hiçbir sıra çizelgeleyiciye bırakılmadı — her
adım ya bir kanal, bir kilit ya da bir bekleme grubuyla zorlandı, ya da sonuçlar toplanıp
sıralandı.
Üç İddia
Bu tablonun okunması üç iddiaya dayanıyor. Birincisi: bir goroutine başlatmak, onun
biteceği anlamına gelmiyor. On ikinin yalnız ikisi kendi durdu; kalan onunun durması
başka birinin yaptığı bir işe bağlı — beşi bir adımın gelmesine, ikisi bir iptale, üçü de
hiçbir şeye, çünkü hiç durmadılar. Kaynağa bakan biri go sözcüğünü görür, ama bu on
satırın hangisinin ne zaman duracağını kaynaktan okuyamaz.
İkincisi: durmamak hata vermiyor. Üç sızıntının üçü de sessiz; program normal çıkışla
sonlanıyor, hiçbir uyarı üretilmiyor. Kanıt yalnız dolaylı: terk edilmiş kanala gönderim
şimdi de geçmiyor (denenirMi false döndürüyor) ve taban sayının üstünde goroutine
kalmış (sizintiVarMi true döndürüyor). Sayının kendisi hiçbir yerde basılmıyor, çünkü
runtime.NumGoroutine çalışma zamanının kendi goroutine’lerini de sayabiliyor; kural budur
— sızıntının sayısı hiçbir derste basılmaz, varlığı true/false olarak yazılır.
Üçüncüsü: durduran şey kanal değil, birinin yaptığı iştir. Beş “beklediği geldi” satırı
beş ayrı düzenekle kuruldu — tamponsuz alım, kapanan kanalda aralık, kilit, bekleme grubu,
select. Düzenek her satırda değişiyor; değişmeyen şey bağımlılık: her birinde başka
bir goroutine’in (kimi zaman ana goroutine’in) bir adımı olmasa bekleyen hiç durmayacaktı.
İletişim ile paylaşım arasındaki tercih ilerideki derslerin konusu; burada önemli olan,
hangi düzenek seçilirse seçilsin, durmanın her zaman birinin bir işine bağlı kalması.
Bu üç iddianın kod üzerindeki karşılığı satır satır izlenebilir. Üçüncü ve dördüncü
satırlar (03, 04) aynı sınıfa girer ama farklı düzenekler kullanır: biri tamponsuz bir
kanaldan tek değerlik bir alım, öteki kapanmış bir kanalda tükenene kadar süren bir
range. Beşinci ve altıncı satırlar (05, 06) da “beklediği geldi” sınıfındadır ama
beklenen şey bu kez bir değer değil, bir durumdur — kilidin boşalması, sayacın
sıfırlanması. Yedinci satır (07) ise select ile birden çok olası kaynaktan yalnız
hazır olanını seçiyor; bu derste iki kanaldan biri önceden dolduruldu, bu yüzden seçim
belirlenimli — iki kanal da aynı anda hazır olsaydı seçim rastgele olurdu, ve bu durum
dördüncü derste ayrıca ölçülüyor.
Sınırlayıcı Ölçüm: “Kendi Durdu” da Bir Sözleşmedir
GK3. — “Kendi durdu” sınıfı en kolay göründüğü hâlde bir sözleşmesizlik taşımıyor değil.
Birinci ve ikinci goroutine gövdelerini bitirdiler ve kendiliğinden durdular — ama bu
bitişi kimseye bildirmediler; d.bildir çağrısı olmasaydı, ana goroutine’in bu ikisinin
bitip bitmediğini bilmesinin hiçbir yolu olmazdı. Ölçümün kendisi bunu kullanıyor: on iki
goroutine’in dokuzu raporunu bir kanala yazdığı için sayılabildi; ölçüm bu dokuzu kaç
tane geleceğini önceden bilerek bekliyor (d.topla(9)), gelmeyenleri değil.
Bu, bir goroutine’in bittiğini bilmenin, onun bitmesinden ayrı bir iş olduğunu gösteriyor. Bir goroutine kendi gövdesini bitirdiğinde dil hiçbir şey yapmaz — ne bir olay yayınlar, ne bir bayrak çevirir. Bitmiş olmak, dışarıdan görülür bir şey değildir; görülür olması için goroutine’in kendisinin, tıpkı bu ölçümde olduğu gibi, bir kanala yazması gerekir. “Kendi durdu” ile “hiç durmadı” arasındaki fark bu yüzden dışarıdan bakan biri için bazen görünmez: ikisi de sessizdir, tek fark biri gerçekten bitmiş, öteki hâlâ çalışıyor olmasıdır.
Bunun pratik sonucu, “goroutine’i başlattım, gövdesi kısa, zaten bitmiştir” varsayımının
hiçbir zaman kaynaktan doğrulanamamasıdır. Birinci ve ikinci goroutine’in kaynak kodu bunu
söylemiyor; onların bittiğini bilen tek şey, bu dersin ölçümünün kurduğu kanaldır. Aynı
gövde, d.bildir çağrısı olmadan yazılsaydı, derleyici hiçbir şey değiştirmezdi ama ölçüm
kendisi imkânsız olurdu — dokuzuncu satırdan sonrasını beklemenin hiçbir yolu kalmazdı.
Özet
- Bir goroutine başlatmak (
go f()) ile onun durması ayrı iki iştir;gosözcüğü durma koşulunu hiç söylemez. - Bir goroutine dört biçimde durur: kendi gövdesini bitirerek (kendi), başka bir goroutine’in adımını bekleyerek (beklediği geldi), bir bağlamın iptaliyle (iptal), ya da hiç durmayarak (hiç durmadı).
- On iki goroutine’in 2’si kendi, 5’i beklediği geldi, 2’si iptal, 3’ü hiç durmadı — bu dağılım kursun bağlayıcı sayısı.
- goroutine bir iş parçacığı değildir; dilin kendi çalışma zamanı goroutine’leri az sayıda gerçek iş parçacığı üzerinde kendi zamanlayıcısıyla yürütür.
- Sızıntı hiçbir derste sayıyla basılmaz, yalnız varlığı
true/falseolarak yazılır. - “Kendi durdu” da bir sözleşmedir: bir goroutine’in bittiğini bilmek, onun bitmesinden ayrı bir iştir ve görünür olması için ayrıca bildirilmesi gerekir.
Sonraki Adım
Beş “beklediği geldi” satırının ilki bir kanaldan geldi: kanal <- 1 yazıldı, karşı
tarafta bekleyen goroutine bunu aldı ve durdu. Ama bu satırda iki şey birden oldu — bir
değer aktarıldı ve iki goroutine bir noktada buluştu. Sıradaki ders bu iki işi ayırıyor:
bir kanal aynı anda hem bir değer taşıyor hem de göndereni ve alıcıyı eşzamanlıyor, ve bu
ikisi her zaman birlikte gelmek zorunda değil.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.