- Go’da yalnızca goroutine ve kanallar ile ifade edildiğinde doğal durmayan eşzamanlılık kalıpları vardır; coroutine’ler, paralellik olmadan yürütme akışını açıkça devrederek programı yapılandırmaya yardımcı olan bir öneridir
- Coroutine’ler
resume ve yield ile yürütme hakkını birbirine devreder; aynı anda yalnızca biri çalıştığı için paylaşılan veri yarışlarından kaçınılır ve geçiş noktaları senkronizasyon noktası olur
- Python generator’ları ve CLU iterator’ları coroutine’lere benzese de
yield konumu sınırlıdır; bu yüzden Lua tarzı iç içe ağaç dolaşımını olduğu gibi taşıdığınızda bazı değerlerin kaybolması gibi bir fark ortaya çıkar
- Go’daki
coro.New, kanallar ve goroutine’lerle de ifade edilebilir; coro.Pull ise push iterator’ı, her çağrıldığında tek bir değer çıkaran pull iterator’a dönüştürür
- Kanal tabanlı uygulama geçiş başına yaklaşık 190 ns idi; çalışma zamanı üzerinden doğrudan geçiş ise geçiş başına yaklaşık 20 ns’ye,
coro.Pull için değer başına yaklaşık 40 ns’ye kadar düşerek pratik kullanımda darboğaz yaratmayacak bir seviyeyi hedefler
Coroutine’lerin yürütme modeli
- Coroutine’ler işlev çağrısı gibi görünür, ancak farklı yığınlarda çalışır ve aynı anda yürütülmez
F, Gyi başlatsa bile G hemen çalışmaz; çalışması için Fnin açıkça resume etmesi gerekir
G, çalışırken istediği anda yield ile yürütme hakkını Fye geri verebilir
G döndüğünde temizlenir ve F, artık Gyi resume etmemesi gerektiğine dair bir sinyal alır
- Bu modelde aynı anda yalnızca bir coroutine çalışır ve çağıran taraf başka bir yığında bekler
- Yürütme geçişleri yalnızca programın belirli noktalarında gerçekleştiği için birden fazla akış koordineli biçimde sırayla çalışır
Lua örneğiyle coroutine’ler
- Lua 5 örneği, yapıları farklı olan iki ikili ağacın aynı değer dizisine sahip olup olmadığını karşılaştırır
t1 ve t2 1, 2, 3, 4, 5 değerlerini içerir
t3 1, 2, 3, 4, 6 değerlerini içerir
visit(t), ağacı inorder dolaşarak her değeri coroutine.yield(t.value) ile dışarı verir
- Karşılaştırma işlevi iki
visit coroutine’i oluşturur ve sırayla coroutine.resume ederek sonraki değeri okur
- İki coroutine’in bitiş durumu ya da değerleri farklıysa
false
- İkisi de bittiyse
true
- Daha deyimsel Lua kodu,
coroutine.wrap ile coroutine nesnesini gizleyen bir next işlevi elde eder
- Coroutine bittiğinde
next işlevi nil döndürür
- Tam kod Gist’te yer alır
Python generator’larının ve CLU iterator’larının sınırları
- Python generator’ları Lua coroutine’lerine benzer görünür, ancak aynı model değildir
- Lua örneğini doğrudan Python’a taşırsanız
visit(t['left']) gerçek dolaşımı çalıştırmaz; yalnızca bir generator nesnesi oluşturup atar
- İşlev gövdesinde
yield varsa def visit sıradan bir işlev değil, generator tanımlar
- Basit çeviri örneği ağaçtan yalnızca 4’ü yazdırır ve 1, 2, 3, 5 değerlerini kaybeder
- Doğru Python kodu, iç içe generator’ı doğrudan yinelemeli ve tekrar
yield etmelidir
- Python 3.3’teki
yield from bu kalıbı basitleştirir
- Python generator nesnesi tek bir
visit çağrısının durumunu tutar
- Yerel değişken değerleri ve çalışılan satır generator nesnesinde saklanır
- Devam ettirildiğinde bu durum çağrı yığınına çıkar,
yield sırasında yeniden generator nesnesine geri düşer
yield yalnızca en üst çağrı çerçevesinde mümkündür
- CLU bu soyutlamaya iterator adını verdi ve statik olarak
iter ile procu ayırdı
- Tür bilgisi sayesinde, iterator’ı sıradan bir işlev gibi hatalı çağıran kullanımları derleyici teşhis edebiliyordu
- Barbara Liskov ve diğerlerinin 1977 tarihli “Abstraction Mechanisms in CLU” makalesi, iterator’ın sınırlı bir coroutine biçimi olduğu için yalnızca program yığınıyla uygulanabildiğini açıklar
Coroutine, thread ve generator farkı
- Üç kavram da bir tür eşzamanlılık sağlar, ancak sundukları güç ve maliyet farklıdır
-
Coroutine
- Paralellik olmadan eşzamanlılık sağlar
- Bir coroutine çalışıyorsa, onu devam ettirmiş olan ya da ona yürütmeyi bırakmış olan coroutine çalışmaz
- Geçiş noktaları açık olduğu için veri paylaşımında yarış oluşmaz
coroutine.resume veya next çağrısı gibi geçişler senkronizasyon noktası olur ve happens-before edge oluşturur
- İşletim sistemi olmadan açıkça zamanlandığı için geçişler yaklaşık 10 ns veya altına kadar inebilir
-
Thread
- Coroutine’den daha güçlüdür; ek güç paralelliktir
- Bunun bedeli zamanlama ek yükü, daha pahalı context switch ve bir tür preemption gereksinimidir
- Tipik bir thread geçişi birkaç mikrosaniye düzeyindedir
-
Go goroutine
- Bu sınıflandırmada ucuz thread’e daha yakındır
- Go runtime zamanlamanın bir kısmını üstlendiği için geçişler birkaç yüz ns’ye yakındır
- Thread’ler gibi paralellik ve preemption sağlar
- Java’nın yeni lightweight thread’i de temelde goroutine ile aynıdır
-
Generator
Go’da coroutine gereken durumlar
- Go’nun mevcut eşzamanlılık kütüphaneleri coroutine kalıbını doğrudan sunmaz
- Goroutine’ler çoğu zaman yeterince benzer olsa da paralellik ve preemption nedeniyle coroutine’lerden farklı sonuçlar üretebilir
- Rob Pike’ın 2011 tarihli “Lexical Scanning in Go” sunumu,
text/template paketinin ilk lexer ve parser tasarımını ele alır
- Lexer ve parser ayrı goroutine’lerde çalışır ve kanallarla bağlanır
- Bu, bir coroutine çiftini eksik biçimde taklit eden bir yapıydı
- Lexer, parser son token’ı işlerken sonraki token’a önceden bakıyordu
- Generator, birden fazla işlevden değer
yield etmesi gereken lexer için yeterli değildi
- Goroutine’in paralelliği yarışlara yol açtı ve sonunda lexer durumunu bir nesnede saklayan tasarıma geçildi
- Uygun bir coroutine olsaydı yarışlardan kaçınılır ve goroutine’den daha verimli olurdu
- Gelecekteki kullanım örneklerinden biri generic collection dolaşımıdır
- Go’da işlevler için range desteği daha önce tartışılmıştı
- Bu, koleksiyon ve soyutlama yazarlarını CLU tarzı iterator işlevleri sunmaya yöneltebilir
- Go’da bugün de işlev değerleri kullanılarak push iterator uygulanabilir
- Örnek:
func (t *Tree[V]) All(yield func(v V))
- Şu anda
t.All(func(v V) { fmt.Println(v) }) şeklinde çağrılabilir
- Gelecekte
for v := range t.All biçimi mümkün olabilir
- Tek bir
for döngüsüne sığmayan dolaşımlar sorun yaratır
- İkili ağaç karşılaştırmasında olduğu gibi iki dolaşımı birbirinin arasına yerleştirmeniz gereken durumlar vardır
- Coroutine,
(*Tree).All gibi bir push iterator’ı, her çağrıda tek bir değer döndüren pull iterator’a dönüştürebilir
Saf Go ile ifade edilen coro.New
- Go’ya coroutine eklenecekse, bunun dil değişikliği olmadan mümkün olması ve sıradan Go kodu olarak anlaşılabilir ve uygulanabilir olması gerekir
- Basit bir
coro.New, channel ve goroutine ile ifade edilir
cin giriş değerini iletir
cout çıkış değerini geri döndürür
resume, cine değer gönderir ve couttan sonucu bekler
- Yeni goroutine başlangıçta
<-cin üzerinde bloke olduğundan paralel çalıştırma fırsatı yoktur
yield eklendiğinde f çalışırken değer dışarı verir ve çağıran taraf bir sonraki resumeda tekrar değer koyabilir
yield(out), couta değer gönderir ve cinden bir sonraki girişi bekler
- Bu da bir send-receive çifti olduğundan paralellik yoktur
- Bu iletişim deseni goroutine’i coroutine gibi davranacak şekilde sınırlar
- Gerçekte goroutine’dir, ancak
resume ve yield geçiş işlemleri rolünü üstlenir
Dize ayrıştırıcı örneği
- “Storing Data in Control Flow” yazısındaki problem,
func parseQuoted(read func() byte) bool fonksiyonunu ayrı bir kontrol akışında çalıştırıp baytları Write metodu ile tek tek beslemektir
coro.New kullanıldığında, önceki yazıdaki geçici channel tabanlı uygulamaya göre daha yüksek seviyede yazılabilir
Init, coparse fonksiyonunu tanımlar
read, NeedMoreInput değerini yield ettikten sonra çağıranın gönderdiği baytı döndürür
parseQuoted(read) fonksiyonunun boolean sonucu BadInput veya Success değerine dönüşür
p.resume(0), parseQuoted içindeki ilk read çağrısına kadar ilerletir
Write(c byte), p.resume(c) çağıran ince bir wrapper olur
- Kodun tamamı Go Playground’da bulunuyor
Asal sayı eleği örneği
- Doug McIlroy’un eşzamanlı asal sayı eleği, her asal
p için bir coroutine içeren bir pipeline’dır
- Her filtre sol komşusundan sayı alır; sayı
p ile tam bölünmüyorsa sağ komşusuna aktarır
- Sol uçtaki counter 2, 3, 4, ... değerlerini sağlar
- Sağ uçtaki çıktı coroutine’i asal sayıları okuyup yazdırır ve yeni bir filtre coroutine’i oluşturur
counter, değerleri yield eden bir döngüyü coro.New ile saran bir fonksiyondur
more bool, üretmeye devam edilip edilmeyeceğini iletir
yield(i), değeri dışarı verir ve devam edilip edilmeyeceğine dair sonraki bilgiyi alır
filter(p, next), sol coroutine’in next(true) çağrısından değer alır ve yalnızca n%p != 0 olduğunda yield(n) yapar
main, mevcut pipeline çıktısını next içinde tutar
- Asal
pyi okur
pyi yazdırır
pnin katlarını kaldıran yeni bir filtreyi pipeline’ın sağına ekler
- Coroutine’ler arasındaki çağrı ilişkisi çalışma sırasında değişebilir
- counter’ın ilk
yield çağrısı maine gider, ancak sonraki yield çağrıları 2 filtresine gider
- Her
p filtresinin ilk çıktısı bir sonraki asal olarak maine gider; sonraki çıktılar bir sonraki filtreye gider
- Kodun tamamı Go Playground’da bulunuyor
goroutine ve coroutine ilişkisi
- Burada oluşturulan kontrol akışı, kesin konuşmak gerekirse bir goroutine’dir
- mutex, channel, system call bekleme gibi sıradan bir goroutine’in yapabildiği her şeyi yapabilir
coro.New, yield ve resume içinde coroutine geçiş işlemlerini kullanabilen bir goroutine oluşturur
go ifadesi yeni bir eşzamanlı ve paralel kontrol akışı oluştururken, coro.New yeni bir eşzamanlı ama paralel olmayan kontrol akışı oluşturur
- 10 adet
go ifadesi çalıştırılırsa main dahil 11 goroutine aynı anda çalışabilir
coro.New 10 kez çağrılırsa kontrol akışı sayısı 11 olur, ancak programın paralelliği değişmez ve aynı anda yalnızca biri çalışır
- Hangi goroutine’in “paralel olmayan” coroutine rolünü üstlendiği çalışma sırasında değişebilir
- Bu, çalışma sırasında hangi goroutine’in channel gönderimi veya alımı içinde olduğunun değişebilmesine benzer
Daha sağlam resume
- İlk
coro.New uygulamasında fonksiyon bittikten sonra resume çağrılırsa deadlock oluşur
- Bunu düzeltmek için
resume, sonuçla birlikte bir bool döndürür
true, sonucun yieldten geldiği anlamına gelir
- Fonksiyon dönerse
resume, dönüş değerini ve false değerini döndürür
- Coroutine sonlandıktan sonra
resume çağrılırsa zero value ve false döndürür
running değişkeni, fnin çalışıp çalışmadığını izler
resume ve coroutine sırayla çalıştığı için running paylaşımı race değildir
- Örnek
"hello" true, "world" true, "done" false, "" false çıktısını üretir
Iterator dönüşümü için coro.Pull
coro.Pull, push iterator’ı pull iterator’a dönüştürür
- Girdi push iterator’ın biçimi şöyledir
push func(yield func(V) bool)
yieldin boolean dönüş değeri devam edilip edilmeyeceğini gösterir
- Hedef pull iterator’ın biçimi şöyledir
pull func() (V, bool)
- channel receive veya map lookup gibi değeri ve yinelemenin bitip bitmediğini döndürür
- Erken durdurma için
Pull, yalnızca pull değil stop da döndürür
- Uygulama için push iterator’ı
coro.New ile çalıştıran küçük bir wrapper yazmak yeterlidir
pull, resume(true) çağırır
stop, resume(false) çağırır
- Ağacın
All metodu, yieldin bool sonucunu kullanacak şekilde değiştirilir
- Sol dolaşım, mevcut değeri yield etme ve sağ dolaşım
&& ile bağlanarak erken durdurma yayılır
- Ağaç karşılaştırma fonksiyonu iki
coro.Pull oluşturur ve değerleri tek tek karşılaştırır
defer stop1() ve defer stop2() ile erken çıkışta coroutine’ler durdurulur
- Değer veya bitiş durumu farklıysa
false
- İkisi de biterse
true
- Kodun tamamı Go Playground’de bulunuyor
panic yayılımı ve iptal
- Coroutine’de oluşan panic, o coroutine’i en son
resume eden çağırana geri gönderilebilir
- Normal bir goroutine’de hangi goroutine’e bildirileceğini ve o goroutine’in bunu almaya hazır olup olmadığını bilmek zordur
- Coroutine’de çağıran taraf
resume içinde bloklanıp beklediği için panic’in iletileceği hedef nettir
- Uygulama,
cout üzerinden değer ya da panic içeren bir mesaj iletir
- Yeni coroutine’in
defer’ı panic’i yakalar
- Bekleyen
resume, aynı panic değeriyle yeniden panic yapar
- Örnekte coroutine
"hello" değerini yield ettikten sonra "world" ile panic yapar
- Panic main goroutine’e yayılır ve stack’te
resume çağrısında oluşmuş gibi görünür
- Kodun tamamı Go Playground üzerinde bulunuyor
- Çağıran erken sonlandırdığında coroutine’e bildirmek için bir
cancel fonksiyonu eklenir
cancel, resumea benzer; ancak yieldin bir değer döndürmesi yerine panic yapmasını sağlar
- İptal panic’i,
ErrCanceled koşulunu sağlayan özgün bir error wrapper kullanır
cancelın tetiklediği panic yeniden yayılmaz; ancak iptal sırasında coroutine başka bir panic üretirse bu yayılır
resume henüz çağrılmadıysa cancel, fnin hiç çalıştırılmamasını sağlar
- Iterator’ı durdurmak için açık bir
bool, panic’ten daha net olduğundan Pull bool tabanlı durdurmayı korur
Asal sayı eleğine yeniden bakış: temizleme ve hata yayılımı
- Yeni API’de
counter ve filter, resume fonksiyonu ile cancel fonksiyonunu birlikte döndürür
primes(n int) bir counter oluşturur ve defer cancel() kaydeder
- Her asal sayıyı okur ve yazdırır
- Her yeni filter eklendiğinde, ilgili filter’ın
cancelı da defer ile kaydedilir
- Fonksiyon
n adet asal sayı elde edip döndüğünde, ertelenmiş cancel çağrıları oluşturulan coroutine’leri temizler
- Herhangi bir coroutine panic yaparsa, beklemekte olan coroutine’e yayılır
primesın doğrudan next ile devam ettirdiği bir coroutine ise panic primesa geri döner
- Bir filter’ın
next ile devam ettirdiği bir coroutine ise panic filter zinciri boyunca primes içindeki p := next(true) satırına kadar yükselir
- Ardından
primesın ertelenmiş cancelları kalan coroutine’leri temizler
- Kodun tamamı Go Playground üzerinde bulunuyor
Nihai API biçimi
New, yeni ve duraklatılmış bir coroutine oluşturur ve f fonksiyonunu çalıştırmaya hazırlar
- Yeni coroutine bir goroutine’dir, ancak kendi kendine çalışmaz
- Yalnızca başka bir goroutine
resume veya cancel çağırıp beklerken çalışır
resume(in), çağıran goroutine’i durdurur ve yeni coroutine’e geçiş yapar
- İlk çağrı
f(in, yield) çalışmasını başlatır
f, yield(out) çağırana veya out döndürene kadar resume bloklanır
yield çağrıldığında resume, out, true döndürür
f döndüğünde resume, out, false döndürür
- Sonraki
resume(in), bloklanmış olan yieldin in döndürmesini sağlar
cancel, f çalışmasını durdurur ve coroutine’i sonlandırır
resume hiç çağrılmadıysa f çalıştırılmaz
- Aksi halde bloklanmış olan
yield, ErrCanceled koşulunu sağlayan bir error ile panic yapar
f recover edemediği bir panic üretirse, bu panic resume veya cancel içinde bekleyen goroutine’e taşınır ve aynı değerle yeniden panic yapar
- Ancak
cancel, kendisinin tetiklediği iptal panic’ini yeniden panic etmez
f döndüğünde veya panic yaptığında coroutine artık var olmaz
- Bundan sonraki
resume çağrıları zero value ve false döndürür
- Bundan sonraki
cancel çağrıları yalnızca geri döner
resume, cancel ve yield başka goroutine’lere aktarılıp kullanılabilir
- Bunun sonucunda hangi goroutine’in “coroutine” olduğu dinamik olarak değişebilir
New yeni bir goroutine oluşturur; ancak her zaman bir goroutine’in resume, cancel, yield veya başlangıç bekleme durumunda bloklanmış olduğu değişmezini korur
- Bu değişmez,
f dönene kadar korunur
- Sonuç olarak
coro.New yeni eşzamanlılık oluşturur, ancak yeni paralellik oluşturmaz
- Nihai imza şöyledir
func New[In, Out any](f func(in In, yield func(Out) In) Out) (resume func(In) (Out, bool), cancel func())
Verimlilik
- Coroutine’leri saf Go uygulamasıyla tanımlayabilmek gerekir; ancak gerçek kullanım için optimize edilmiş bir runtime uygulaması gerekir
- 2019 MacBook Pro’da kanal tabanlı
coro.New, değer gidiş-dönüşünde geçiş başına yaklaşık 190ns sürer
coro.Pull içinde bu, değer başına yaklaşık 380ns eder
coro.Pull, iterator’ın standart kullanım biçimi değildir
- Standart yöntem iterator’ı doğrudan çağırmaktır; bu durumda coroutine overhead’i yoktur
coro.Pull, değerleri tek bir for döngüsüyle değil, kademeli olarak işlemek gerektiğinde gerekir
- İlk optimizasyon denemesi, derleyicinin send-receive çiftlerini işaretlemesi ve runtime’ın bunları tek bir işlemde birleştirmesi için ipucu bırakması yöntemidir
- Kanal runtime’ı scheduler’ı bypass edip doğrudan diğer coroutine’e atlayabilir
- Geçiş başına yaklaşık 118ns, pulled value başına yaklaşık 236ns sürer
- Özgün kanal uygulamasından %38 daha hızlıdır
- İkinci uygulama kanalları tamamen devre dışı bırakır ve runtime’a doğrudan coroutine geçişi ekler
- Coroutine geçişi 3 atomic compare-and-swap işlemine indirgenir
- Biri coroutine veri yapısı, biri bloklanan coroutine’in scheduler status’u, biri de devam ettirilen coroutine’in scheduler status’u için kullanılır
- Geçiş başına yaklaşık 20ns, pulled value başına yaklaşık 40ns sürer
- Özgün kanal uygulamasından yaklaşık 10 kat daha hızlıdır
- Değer başına 40ns,
coro.Pull gereken kodlarda darboğaz olmayacak kadar küçük bir mutlak maliyet olarak kabul edilir
1 yorum
Hacker News yorumları
Görünüşe göre burada asıl noktayı kaçıran çok kişi var. Coroutine kütüphanesinin eşzamanlılığı ele almak için
goanahtar sözcüğünden daha kötü ve zahmetli bir yol olduğu doğru.Bu karmaşıklığı getiren gerçek kullanım alanı fonksiyon iterator’ları; yani
func() (T, bool)türündeki fonksiyonlardarangekullanılabilmesini sağlamak. Bu, Go topluluğunda uzun süredir tartışılıyor ve anlamı çoğu Go programcısına sezgisel gelecektir.Bu yazı bir sonraki sorunu ele alıyor: Fonksiyon iterator’ları dile eklendiğinde,
fordöngülerinde kullanılacak iterator’lar nasıl yazılacak? Push iterator’ların çoğu durumda yazmasının kolay olmasından başlayıp bunu bir push-pull adaptörüne genişletiyor; bu adaptör de coroutine’lerin üzerine kuruluyor.Hepsi eklenirse, coroutine’leri yineleme dışındaki amaçlarla kullanmak, mutex’in yeterli olduğu yerde kanal/goroutine kullanmak kadar kötü bir pratik olacak gibi görünüyor.
İki iş mantıksal olarak senkron biçimde iş birliği yapıyorsa, örneğin bir iterator söz konusuysa, hepsinin aynı CPU üzerinde çalışması çok daha verimlidir. Kernel’in hiçbir şeyi yeniden zamanlaması ya da CPU çekirdeklerini kapatıp uyandırması gerekmez; veri de CPU cache’inde uygun biçimde kalır, bu da cache gecikmesini ve isabet oranını iyileştirir.
Goroutine’lerde bunun tesadüfen böyle olması mümkündür ama garanti değildir; en azından Go runtime’ının goroutine scheduler’ından geçmenin bir maliyeti vardır. Hızlı olsa da aynı goroutine içinde başka bir kod context’ini çalıştırmak kadar hızlı değildir.
Coroutine’lerde A işinin B işine doğrudan geçtiğini bilebildiğiniz için zamanlama davranışı daha öngörülebilirdir. Yazının son kısmında Russ, optimize edilmiş bir runtime coroutine uygulamasının goroutine’lerle taklit edilene göre 10 kat daha hızlı olduğunu gösteriyor.
Google’da şirket içinde bu tür iş birlikçi multithreading’i uygulayan bir kernel patch’i var; içeride buna fibers deniyor. Daha iyi gecikme ve öngörülebilir zamanlama için var. Paul Turner’ın yaklaşık 10 yıl önce LPC’de bu motivasyonu anlattığı bir sunumu da var: https://www.youtube.com/watch?v=KXuZi9aeGTw
for { next := getNext(); ... }şeklinde kullanmanın nesi sorun, anlamıyorum.for next := range getNext { ... }şeklinde kullanmanın avantajı ne, merak ediyorum.rangeya daswitchkullanıp, değerleri kanala iten bir goroutine çalıştırmanın neden olmayacağını anlamıyorum. Coroutine’lere neden ihtiyaç olduğunu hâlâ ikna edici bulmuyorum.Bu kez dikkatli düşünme ve iyi bir 80/20 çözümü değil de, “bunu doğru yapmak için coroutine gerekiyor galiba, o zaman ekleyelim” gibi görünüyor.
Generics eklenirken gerçekten çok uzun ve derin düşünülmüş, dengesi çok iyi olan yenilikçi bir uzlaşma ortaya konmuştu.
Burada da “belirli durumlarda kontrol edebilmek için goroutine’lere bir özellik eklemek” gibi bir yaklaşım beklerdim. “Bu problem için Rust gibi tamamen ileri gidip doğrudan ekleyelim” demekten daha iyi görünürdü.
Go’yu yıllardır profesyonel olarak kullanıyorum ama Python’daki Twisted / Tornado / diğer framework’lere benzemesini istemiyorum.
goanahtar sözcüğü oldukça acı veren fonksiyon renklendirme problemini iyi biçimde engelliyor.Yüksek performans bağlamlarında CPU çekirdeği başına veri sharding’i gibi şeyler yapmak istediğiniz zamanlar oluyor, ama bu öneri o kaşınan yeri kaşımıyor.
async/awaitbenzeri bir şey getirmeye yönelik bir içerik yok.Coroutine’ler, Python’daki generator’lara daha yakın başka bir alanı dolduracak. Birleştirilebilir bileşen pipeline’larını bağlamanız gereken yerlerde bellek kullanımını ve kod karmaşıklığını çoğu zaman ciddi biçimde azaltabilir. Tasarım tamamen senkron görünüyor.
Contextkullanmanız gerekiyorsa, bu kelimenin tam anlamıyla fonksiyon renklendirmedir.Elbette dünyanın bir yerindeki zeki bir geliştiricinin bununla aptalca bir şey yapıp onun büyük popülerlik kazanması neredeyse kesin. Benim tahminim goroutine ve coroutine’leri tek bir soyutlamada birleştirmek olur.
Genelde goroutine ile ilgili kodlar kanallar yüzünden dağınık hissettiriyor ve bunu nasıl daha “temiz” hale getireceğimi bilmiyorum. Bakımı yapılabilir olduğu kanıtlanmış kalıplara dair ipuçları olursa gerçekten minnettar olurum.
Çok görevli sistemler bize süreçleri verdi
Ama bunlar çok ağırdı
Bu yüzden adres alanını, dosya tablosunu ve birkaç şeyi paylaşan süreçler olan thread’ler ortaya çıktı. Zamanlayıcı, süreçler arasında olduğundan thread’ler arasında daha kolay geçiş yapabilir ve thread’ler arasında veri paylaşımı serileştirme gerektirmez
Ama bu da çok ağırdı
Bu yüzden kullanıcı alanı thread’leri ortaya çıktı. Çalışma zamanının tamamen kullanıcı alanında çalıştırdığı mantıksal yürütme thread’leridir. Çalışma zamanı, standart kütüphanedeki tüm giriş/çıkış işlevlerine zamanlama kancaları koyar ya da Unix sinyalleri gibi sistem API’leriyle mantıksal thread’i önceden keser. Sistem düzeyi bağlam değişimi gerekmez ve çok küçük yapılabilirler
Ama bu da çok ağırdı
Bu yüzden coroutine’ler ortaya çıktı. Programcının birbiriyle işbirlikçi biçimde etkileşen mantıksal “thread”ler tanımlamasını sağlar. Bir zamanlayıcının var olduğunu varsaymaz. Programcı doğrudan bir event loop yazar ya da bir kütüphanenin event loop’unu “gerçek” mantıksal thread’den çağırır
Sırada neyin geleceğini merak ediyorum. [İletişen sıralı süreçler][1] açısından bakınca, işbirlikçi coroutine’ler inilebilecek en alt seviye olabilir
[1]: https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf
Eşzamanlılığı destekleyen semantiklere ihtiyaç var. Örneğin dolaşım varsayılan olarak sırasız olmalı; bu, tüm program akış analizine de dayanır ama ilkesel bir engel görünmüyor
Microsoft birkaç yıl önce böyle bir dil üzerinde çalışmamış mıydı? Ada topluluğundan birinin yaptığı ParaSail de var. Bu projelere ne olduğunu, kimsenin kullanıp kullanmadığını merak ediyorum
Green thread’lerin özünün, Python’daki
yieldgibi anahtar sözcükler kullanmadan iyi bir işbirlikçi zamanlama elde etmek olduğunu düşünmüştümGo’nun
çağrı noktalarına ve belirli konumlara devam noktaları eklerşeklindeki tasarım kararının çok iyi bir uzlaşma olduğunu düşünmüştümGiderek donanıma daha yakın denetimi açığa çıkarıyorlar. Bir noktadan sonra bunun sadece Zig’i yeniden yapmak olup olmadığını bilmiyorum. Sırada isteğe bağlı çöp toplayıcı mı var?
yieldveresumeanahtar sözcük değil, sıradan değişkenler. Eğitim amacıyla böyle adlandırılmış sıradan closure referanslarından ibaretlerTuhaf olan kısım, iptal callback’ini oluşturup kullanma kısmı. Go’yu çok fazla kullanmadığım için, bunun düşürülmüş yineleyici durumunu daha hızlı toplamak için bir performans iyileştirmesi mi, yoksa bir kanalda bekleyen goroutine o kanalın tek referansına sahip olduğunda Go o goroutine’i çöp toplamadığı için gerekli bir şey mi olduğunu bilmiyorum
Lua’da böyle bir şeye gerek yok. Coroutine/thread’ler de diğer nesneler gibi çöp toplanır; dolayısıyla tüm referanslar kaybolduğunda, son işlem giriş fonksiyonunun dönüşü değil
yieldolsa bile toplanırGOGC=offortam değişkenini ayarlarsanız çöp toplamayı kapatabilirsinizGOGC hakkında ayrıntılar: https://dave.cheney.net/tag/gogc
Coroutine’lerin kütüphaneden ziyade dil desteği olarak gelmesini tercih ederim sanırım
x := co func(){ var z int; for { z++; yield z } }gibi bir şey ya da buna denk bir biçim düşünüyorumBunun saf Go ile de mümkün olması harika; dil belirtimini karmaşıklaştırmak yerine, optimize edilmiş bir çalışma zamanına sahip standart kütüphane paketi olarak sunmanın cazibesini de anlıyorum. Sonuçta saf Go ile mümkünse başka implementasyonlar da hızlıca bootstrap edilebilir
Go’yu her gün
$workte kullanan biri olarak iki seçeneği de memnuniyetle karşılarım, ama dile gömülü olmasını daha çok tercih ederim. Go’nun eşzamanlılık temel öğeleri her zaman güçlü yönüydü; o yönde ilerlemeye devam edilebilirKütüphane çözümüyse coroutine’i öldürmek ya da programı öldürmek gerekir. Stack’in şeffaf biçimde büyümesini istiyorsanız bunu yalnızca üretilen kod yapabilir. Çünkü stack kullanımını izleyip gerektiğinde büyütmesi gerekir. Bildiğim kadarıyla goroutine’lerde böyle bir şey var
Belki bir kütüphane çözümü de stack’in sonuna bir guard page koyabilir. Oraya ulaşıldığında hata işleyicisi stack’i genişletmeyi deneyebilir. Ama stack değişkeninin işaretçisi tutulmuşsa muhtemelen çalışmayacaktır
yieldparametresi eklenmesini tercih ederimBir fonksiyon değer yield edecekse, sözde bir
yieldparametresi olmalı; yalnızca aynı yield imzasına sahip fonksiyonların içinde ya da imzası olmayan fonksiyonların içinde yield edebilmelidirÖrnek
x := func(:z int) { for { z++; :- z } }gibi bir biçime dönüşebilir.:yield imzası ekler,:-ise değeri yield ederYalnızca
Xdeğeri yield eden bir fonksiyonda sadece: Xveya: name Xolması yeterlidir. Devam ettirildiğindeYtipinde bir değer alacaksa imza:[Y] Xveya:[Y] name XolurKabul gevşek olmalı.
Yile devam ettirilen veXyield eden bir fonksiyon beklenen yerde, devam değeri olmadan yalnızcaXyield eden bir fonksiyon da kabul edilmelidircopaketinin özel fonksiyonlarıresumeveNewişlevlerini sağlarsa Go tarzı korunabilir.rangesöz dizimi, devam değerini-:ile geçirecek şekilde genişletilebilir; değer geçirilmezse varsayılan sıfır değerle devam ettirilebilirPek hoşuma gitmedi. Örneklere bakınca dili okumayı ve takip etmeyi çok daha zorlaştırıyor gibi görünüyor. Elbette bu benim kafamdan ve önyargılarımdan da kaynaklanıyor olabilir
Üstelik mevcut bloklayan kanallarla ya da durumla yapılamayan bir şeyi mümkün kılıyor gibi de görünmüyor
Kritik kod yolunda allocation’dan kaçınmak için bu yazıdakine benzer iterator’lar kullandığım oldu. Bu yaklaşımla o tür kod çok daha az garip hâle gelir. Özellikle de yakında gelecek
rangeiterator dil değişikliğiyle birlikteKanallar, bloklayan işlemleri sarmalamadıkları zaman pek bir sebep yokken yavaş
Yorumları okuyunca içim burkuldu
Birçok kişi coroutine’ler ile green thread’leri neredeyse aynı şey sayıyor, oysa ikisinin de artıları ve eksileri var
Go topluluğunda iterator eksikliğinin kabul edilebilir görülmesi üzücü. Sadelik adına dili az da olsa karmaşıklaştırabilecek özellikler özellikle reddediliyor gibi. Yine de en azından generics konusundaki tutumlarından geri adım attılar
Go’nun yine benim dilim olmadığı fikrine kapılıyorum
Şimdi biraz kullanıyorum; çünkü çağrı noktasında sözdizimsel kolaylık biraz daha iyi. Tanım noktasında ise beklendiği gibi hâlâ çirkin; gerçi Go sözdizimi, diğer dillerde gördüklerim arasında neredeyse en iyilerden biri
Sonuçta “generics yok” şikâyetlerini susturmak için sadelikten epey ödün verildiğini düşünüyorum. İyi bir takas değildi
Yazıda önerilen
coropaketi aynen eklenecek mi? Olabilir, ama muhtemelen tam olarak bu hâliyle değil. Benzer bir şey eklenecek mi? Bahse girsem evet derdimNe kadar sürer? En az 1 yıl, yani Ağustos 2024’teki Go 1.23 sürümü civarı diye düşünüyorum. Biraz daha uzun da sürebilir. Bundan çok daha kısa olması zor bence
Generics’in toplulukta kabul görmesinin nedeni, mevcut kodla tamamen geriye dönük uyumlu olması ve ihtiyaç yoksa güvenle yok sayılabilmesi
Şaşırtıcı olmayan şekilde Go kodlarının çoğu hâlâ bunu yapıyor. Çeşitli “collection” türleri dışında generics’in pratik kullanım alanlarını bulmak pek kolay değil. Zaten çoğu kod birden fazla tipe bakmak zorunda kalmıyor; iki tipin ötesi ise başlı başına nadir
Hatta generics’in eklenmesi, insanların “zorunlu” diye talep ettiği övgüyle anılan özelliklerin, yaygın kabul görmüş iyi bir dilde gerçekte ne kadar az gerekli olduğunu daha geniş topluluğa gösterdi
Nihayet CLU gibi programlama dillerine dikkat edilmesi iyi bir şey gibi görünüyor
Öte yandan .NET ve C++ coroutine’leri ile Symbian C++ ve Active Oberon’daki Active Object deneyimlerime dayanarak, bunun gerçekten Go’ya eklenmeye değer olup olmadığından emin değilim
.NET ekibinin de bu yıl BUILD’de kabul ettiği gibi, zamanı geri alabilselerdi runtime’ın bunu Go tarzında halletmesini tercih ederlerdi. Çünkü birçok geliştirici hâlâ async/await’i anlamakta zorlanıyor
Buna gerçekten ihtiyaç var mı bilmiyorum. Go’daki çoğu senaryo goroutine’lerle yeterince ele alınıyor ve
yield/resumesemantiği için 2 bloklayan kanal yeterliKarmaşıklık uğruna karmaşıklık eklemek gibi görünüyor; ayrıca Go’da zaten olmayan yeni bir yetenek gerçekten ekleyip eklemediği de net değil
Karşılaştırma olarak, yakın tarihli bir sunumda 1 milyon Elixir(BEAM VM) thread’i başlatıp hepsine
"Hello!"mesajı gönderen, ardından her thread’in 0 ile 2 saniye arasında rastgele bir süre bekleyip"Process received message !"yanıtını döndürdüğü bir demo yaptımAynı anda yan tarafta Erlang observer’ı açıp CPU ve bellek tüketimini, ayrıca garbage collection’dan sonra ne kadar hızlı toparlandığını gözlemledim
Buradaki en büyük darboğaz terminalin yetişebilme kapasitesi; ama observer gerçek durumu oldukça doğru yansıtıyor gibi
https://www.youtube.com/watch?v=yxyYKnashR0
Kullanılan kod: https://gist.github.com/pmarreck/4cc8f2f55a561ebce2012085a3a...
Bu tür bir özellik 1980’lerden beri Erlang’da, dolayısıyla Elixir’de de yerleşik olarak vardı. Actor modelini ya da Erlang’ın “efsanevi” implementasyonunu çok duymuşsunuzdur; ama izleme araçları da açıkken bunu gerçekte kaç kişinin gördüğünü bilmiyorum
Go’nun böyle dil düzeyi desteği sunması güzel olurdu; ancak BEAM VM’in thread implementasyonu hem oluşturma hem runtime tüketimi açısından son derece kaynak verimli ve yalnızca immutable değerlere izin vermesinden gelen concurrency kolaylığıyla birleşince, yetişmesi asla kolay olmayacak gibi görünüyor