2 puan yazan GN⁺ 2023-07-18 | 1 yorum | WhatsApp'ta paylaş
  • 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

 
GN⁺ 2023-07-18
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 go anahtar 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 fonksiyonlarda range kullanı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, for dö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.

    • Belirli kullanım alanlarında coroutine’lerin tam goroutine’lerden çok daha verimli olabildiğini de söylemek gerek. Çünkü coroutine’e geçerken context switch ya da yeniden zamanlama gerekmez.
      İ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.
    • Go’daki coroutine’ler, ayrık olay simülasyonunu temiz biçimde tanımlamak için Go’yu bir host dil olarak kullanmayı sağlayabilir gibi geliyor. Şu anda aktörlere yield etmek garip duruyor.
    • Kanallarda range ya da switch kullanı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.
    • Yine de sonunda bunun mutfak lavabosu usulü bir ekleme olduğunu düşünüyorum.
      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.
    go anahtar 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.

    • Coroutine’ler ve goroutine’ler farklı nişleri dolduruyor. Goroutine’ler zaten Twisted benzeri şeylerin kapladığı alanı dolduruyor. Burada Go’ya async/await benzeri 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.
    • Yazının hiçbir yerinde fonksiyon renklendirme problemi sayılabilecek bir öneri yok.
    • Goroutine yazıp Context kullanmanız gerekiyorsa, bu kelimenin tam anlamıyla fonksiyon renklendirmedir.
    • Diğer async/threading framework’lerinin “sorunu”, threading’i iterator/coroutine üzerine inşa etmeleri olabilir. Bu durumda ise bunlar birbirine ortogonal, dolayısıyla sanıldığı kadar kötü olmayabilir.
      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.
    • Daha iyi kanal yönetimi kalıpları ya da framework’leri hakkında ipuçları paylaşabilir misiniz, merak ediyorum.
      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

    • Coroutine’ler de programcıyı, bilgisayara mümkün olan her ayrıntıyı anlatma korkusundan kurtarmıyor. Özellikle girdi, programcının düşündüğünden azıcık farklı olsa bile yürütmenin sonsuza dek sürmeyeceğini açıklamak gerekiyor
    • Kodu mümkün olduğunca otomatik paralelleştiren, ama bunu yalnızca ölçülen performansın kazanç gösterdiği ölçüde yapan dilin pek olmaması biraz şaşırtıcı
      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
    • Daha aşağı inecek yer olmadığını düşünüyorum. Bunun yerine dağıtık bilişim tarafına doğru yukarı çıkmak için alan olabilir. Yanlış hatırlamıyorsam Go’nun erken alfa sürümlerinden bazılarında kanallar makineler arasında da çalışıyordu
    • Bir sonraki aşama “bu iş öğeleri koleksiyonunu istediğin şekilde işle” gibi bir biçim olabilir. Biraz gözleri kısarak bakınca, tüm eşzamanlı yürütme işlerin bir sıra dizisi olarak görülebilir; işin kendisi de bir koleksiyon olabilir. Yalnızca bir ya da iki iş olsa bile durum böyledir
  • Green thread’lerin özünün, Python’daki yield gibi anahtar sözcükler kullanmadan iyi bir işbirlikçi zamanlama elde etmek olduğunu düşünmüştüm
    Go’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üm
    Giderek 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?

    • Burada yield ve resume anahtar sözcük değil, sıradan değişkenler. Eğitim amacıyla böyle adlandırılmış sıradan closure referanslarından ibaretler
      Tuhaf 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 yield olsa bile toplanır
    • Go’nun çöp toplayıcısı isteğe bağlıdır. GOGC=off ortam değişkenini ayarlarsanız çöp toplamayı kapatabilirsiniz
      GOGC 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üyorum
    Bunun 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 edilebilir

    • Coroutine’ler dil desteği gerektirir. Bir coroutine’de stack tükenirse ne yapılacak?
      Kü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
    • Mevcut tip sistemiyle daha iyi etkileşmesi için fonksiyonlara yield parametresi eklenmesini tercih ederim
      Bir fonksiyon değer yield edecekse, sözde bir yield parametresi 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 eder
      Yalnızca X değeri yield eden bir fonksiyonda sadece : X veya : name X olması yeterlidir. Devam ettirildiğinde Y tipinde bir değer alacaksa imza :[Y] X veya :[Y] name X olur
      Kabul gevşek olmalı. Y ile devam ettirilen ve X yield eden bir fonksiyon beklenen yerde, devam değeri olmadan yalnızca X yield eden bir fonksiyon da kabul edilmelidir
      co paketinin özel fonksiyonları resume ve New işlevlerini sağlarsa Go tarzı korunabilir. range söz dizimi, devam değerini -: ile geçirecek şekilde genişletilebilir; değer geçirilmezse varsayılan sıfır değerle devam ettirilebilir
    • Bu gerçekten berbat. Hiç sezgisel değil ve Go’nun güçlü yanlarından biriyle de uyuşmuyor. Böyle bir kod yazmak ya da okumak için Go’nun coroutine semantiklerini ayrıca öğrenmek gerekir
  • Pek 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

    • Aynen öyle. Bunu savunanlar sanki sadece burunlarının ucunu görüyor
    • Hangi dil değişikliğinden bahsettiğinizi bilmiyorum. Bu, insanların zaten “durum” ile yaptığı şeyi standartlaştırıp verimli hâle getirmeye yönelik bir öneriden ibaret
      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 range iterator dil değişikliğiyle birlikte
    • Mevcut standart kütüphanedeki gibi birbiriyle uyumsuz düzinelerce iterator implementasyonunun olması da daha kolay değil
      Kanallar, 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

    • Go’yu her gün kullanıyorum ama dürüst olmak gerekirse generics kodumu pek değiştirmedi
      Ş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
    • HN’i “Go topluluğu” ile karıştırmamak gerekir. Bu yazıyı yazan kişi Go ekibinin başındaki kişi
      Yazıda önerilen coro paketi aynen eklenecek mi? Olabilir, ama muhtemelen tam olarak bu hâliyle değil. Benzer bir şey eklenecek mi? Bahse girsem evet derdim
      Ne 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 konusunda tutumlarından geri adım atmış değiller
      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
    • Generics kod tabanımın %20’sini bile değiştirmedi; o %20’nin içinde kütüphaneler de vardı. Go alışkanlığı mı C alışkanlığı mı bilmiyorum ama generics bana bir yerlerde bir programcının bir sorunu çözmüş olduğu başka bir kütüphane gibi görünüyor
    • Go topluluğunun birleşik bir iteration arayüzünü memnuniyetle karşılayacağını sanırdım
  • 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/resume semantiği için 2 bloklayan kanal yeterli
    Karmaşı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

    • Goroutine’ler ve kanallar muazzam overhead ekliyor. Iterator olarak kullanmak fiilen mantıksız
    • Bu konu yazıda ele alınıyor
  • 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ım
    Aynı 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