İçeriğe geç
academia.sh

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; go sö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/false olarak 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat