1 puan yazan GN⁺ 2024-08-29 | 1 yorum | WhatsApp'ta paylaş
  • DoltHub, Go eşzamanlılığında sık kullanılan channel yapısını bilerek aşırı iç içe geçirerek, channel'ı channel üzerinden gönderme üzerine şakacı bir örnek oluşturdu
  • Gerçekte devralınan kodda chan chan struct{} vardı ve worker goroutine'lerine yeni channel'lar aktarmak için fan-out deseni kullanılıyordu, ancak akıl yürütmesi ve yönetimi zor olduğu için yeniden yazıldı
  • Örnek, C ailesindeki int**** “4-star programmer” şakasını Go'nun chan yapısına uyarlayarak, en üst channel olarak _4chan:= make(chan chan chan chan int) kullanıyor
  • factor = 3 iken her channel katmanında producer ve consumer'lar dallanıyor, son int değerleri toplanarak 3'ün 5. kuvveti olan 243 yazdırılıyor
  • Pratikte uygulama ve debug zorluğu, channel kapatma yönetimi, sync.WaitGroup gereksinimi ve goroutine sızıntısı nedeniyle uygun değil; örnek, kapanış mantığı yerine time.Sleep() kullanıyor

Dolt'tan çıkan iç içe channel örneği

  • DoltHub, dünyanın ilk sürüm kontrollü SQL veritabanı olan Dolt'u Go ile yazıyor
  • Tipik bir Go kod tabanı gibi, eşzamanlı çalışmayı uygulamak için channels ve goroutines kullanıyor
  • Eşzamanlı programlama başlı başına zor olduğundan, genelde channel ve goroutine'ler basit ve sezgisel biçimlerde ele alınıyor
  • Bir dönem başka bir açık kaynak projeden alınan kodda, aşağıdaki gibi channel gönderen bir channel vardı
    • var c chan chan struct{}
  • Bu yapı, goroutine'ler arasında channel aktararak worker goroutine'ler için fan-out deseni kurmanın bir yoluydu
    • Ara channel, yeni oluşturulan channel'ı gerçek işi yapan worker'a aktaran aracı görevi görüyordu
    • Çalışıyordu, ancak özellikle goroutine sızıntısı da hesaba katılınca akıl yürütmesi ve yönetimi zordu
    • İlgili kod yeniden yazıldı ve chan chan struct{} ortadan kalktı

“4-star programmer” şakasının Go sürümü

  • C ve türevlerinin yaygın olduğu dönemde, pointer'ları anlamakta zorlanan yeni başlayanlarla ilgili “4-star programmer” diye bir şaka vardı
  • Tipik örnek, int**** gibi çok katmanlı pointer dolaylı başvurusunun kullanıldığı koddur
  • Go da büyük ölçüde C'den türediği için pointer'larla aynı türde kod yazılabilir
    • Sırasıyla *int, **int, ***int, ****int geçirilir
    • Son fonksiyonda ****i = 100 yapılırsa program i is now 100 çıktısını verir
  • Go'da C'de olmayan chan bulunduğundan, aynı şaka channel dolaylı başvurusu üzerinden genişletilebilir

4 katmanlı channel ile 5. kuvvet hesaplamak

  • En üst channel şu şekilde tanımlanıyor
    • _4chan := make(chan chan chan chan int)
  • Go tanımlayıcıları sayı ile başlayamadığı için örnekte _4chan adı kullanılıyor
  • _4chan üzerinden gönderilen değer, 3 katmanlı bir channel
    • _3chan := make(chan chan chan int)
  • Aynı şekilde katmanlar aşağı inerken en sonunda değer channel'ı olan chan int'e ulaşılıyor
  • Her dolaylı başvuru katmanında, sabit factor değerine göre producer oluşturuluyor
    • Örnekte const factor = 3
    • sendChanChanChan, 3-channel producer'ı bir goroutine olarak başlatıyor
  • Consumer tarafı da her katmanda gelen channel'ı alıp bir sonraki katman için factor kadar consumer başlatıyor
    • receiveChanChanChan, _4chan üzerinden _3chan alıyor ve 3-channel consumer'ını başlatıyor

Son katmanda değer gönderimi ve toplama

  • En alt katmanda artık channel değil, gerçek int değerleri gönderiliyor
  • send fonksiyonu, _2chan üzerinden _1chan gönderip ardından factor kadar int producer başlatıyor
  • Her int producer da tekrar factor kadar goroutine oluşturup _1chan <- 1 işlemini yapıyor
  • Consumer, aldığı tam sayıları global sum değişkenine ekliyor
    • sum, atomic.Int32 olarak tanımlı
    • receive(c chan int), channel'dan değer alıp sum.Add(int32(s)) çalıştırıyor

Çalışma sonucu ve dallanma sayısı

  • Tüm program _4chan oluşturuyor, gönderim ve alım katmanlarını ayrı ayrı goroutine olarak başlatıyor, ardından 500 * time.Millisecond boyunca bekliyor
  • Örnek çıktı şöyle
    • 3 ^ 5: 243
  • Bu program, bir sayının 5. kuvvetini mümkün olduğunca dağıtık bir biçimde hesaplayan genelleştirilmiş bir örnek
  • Çalıştırılabilir örnek Go Playground'da görülebilir, sözdizimi vurgulu sürümü ise GitHub Gist'te yer alıyor
  • Daha büyük factor kullanmak istenirse, çalışmanın tamamlanabilmesi için Sleep süresini artırmak gerekebilir
  • Log açıldığında her channel üretim/tüketim katmanındaki dallanma sayıları görülebilir
    • starting 3chan producer: 3 kez
    • starting 2chan producer: 9 kez
    • starting 3chan consumer: 9 kez
    • starting 2chan consumer: 27 kez
    • starting chan producer: 27 kez
    • starting 1chan consumer: 81 kez
    • starting int producer: 81 kez
    • sending int: 243 kez
    • received int: 243 kez

Neden gerçek kodda kaçınılmalı

  • Bu yaklaşım, gerçek kodda kullanılmak için hem uygulama hem de debug açısından zahmetli
  • Channel'ı channel üzerinden gönderince, her bir channel'ın ne zaman kapatılması gerektiğini belirlemek zorlaşıyor
  • Gerçek bir kullanım senaryosunda channel'ların kapatılması gerekir, ancak kapanış mantığını eklemek için tüm channel gönderimlerinin bitip bitmediğini takip etmek gerekir
  • Kapanış yönetimini uygulamak için birçok yere sync.WaitGroup eklemek gerekti ve bu da şaka amaçlı örneğin okunmasını zorlaştırdı
  • Nihai örnek, kapanış mantığı yerine time.Sleep() kullanacak ve çok sayıda goroutine sızıntısı bırakacak şekilde basitleştirildi

1 yorum

 
GN⁺ 2024-08-29
Hacker News görüşleri
  • Gerçek profesyonel yazılım mühendisleriyle yakından çalışan bir bilim insanı olarak, yaptıkları işlerin çoğu bana böyle görünüyor ve neden öyle yaptıklarını anlamak gerçekten zor geliyor.
    Tek bir kod satırı gerçekten çağrılmadan önce sırayla 4 arayüz fonksiyonundan geçiyor; bu fonksiyonların da farklı klasörlerdeki farklı dosyalara dağılmış olduğunu gördüm.
    Bu yüzden kodun ne yaptığını okumak yorucu hale geliyor; birkaç seviye içeri girdikten sonra doğru yere mi bakıyorum, bir noktada gerçek hesaplamanın yapıldığı yere ulaşacak mıyım diye şüphe etmeye başlıyorum.

    • Bu gerçekten kötü bir pratik ve yazılımı fazla hevesli bir junior mühendisin yazma biçimine daha yakın.
      Aşırı ve kafa karıştırıcı göründüğü hissi yanlış değil; ilk “ilginç” kodu yazarken teknik olarak karmaşık, hatta zarif görünebilir ama gerçekten büyümesi gereken bir yazılımın içinde teknik bir kâbusa dönüşür.
      Go kodunda kanalların yanlış kullanımını neredeyse 2 yıl boyunca temizlediğim olmuştu; kanallara gerçekten ihtiyaç duyulan durumlar nadirdir ama başta birçok amaç için kolayca kullanılabildikleri için sorun çıkarırlar.
      Kanalları doğru kullanıp kullanmadığını anlamanın ölçütü, “Doğrudan fonksiyon çağrısıyla yapılamaz mı?”, “WaitGroup ya da mutex ile yapılamaz mı?” sorularına hayır diyebilmek ve “Eşzamanlılık/paralellik kazancı, eşzamanlı kodu debug etmenin karmaşıklığını haklı çıkaracak kadar büyük mü?” sorusuna evet diyebilmektir.
    • Daha kötüsü de var. Bu çok abartılı bir örnek sayılmaz: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
      Anlayış ve yetkinlik olması gereken yerde ürkütücü derecede eksiklik görülebiliyor.
      Lisans yıllarında bir bilgisayar bilimi doktoruyla yaklaşık 30 dakika pair programming yapmıştım; epey aydınlatıcıydı.
      Yazılımdan hiç anlamıyordu; standart kütüphane veri yapılarının boyutunun negatif olup olmadığını kontrol etmediğim için beni uyaracak kadar.
      Yine de bazen bu tür yapıların da bir nedeni olur. Kimi zaman gerçekten makuldür, kimi zaman da önce gelenlerin yarattığı çılgın bir kod tabanıyla baş etme biçimidir.
    • Kod yazanların spektrumunun bir ucunda bilim insanları, diğer ucunda yazılım mühendisleri var. Bizi kurtarabilecek tek şey denge.
      Araştırma makalelerinde kullanılan kodları okuduğum oldu; teorik matematik çoğu zaman zaten kavrayışın ötesindedir, mantığı görmek için koda indiğinizde ise durum çok daha kötü ve anlaşılmaz olabiliyor.
      Sonuçta kendi yaptığımız yönteme alışkınız; başka yöntemler bize yabancı geliyor.
    • Aşırı mühendislik yaygın bir neden. Basit çözüm çoğu zaman bulunması zor şekilde saklıdır.
      Buna rağmen ek dolaylılık katmanları genellikle genel mimari içinde gerekçelendirilir; makul bir durumda, yalnızca yerel olarak bakıldığında değerini görmek zor olabilir.
      “Erken dönem programlama dillerinin hafif dokunuşu ve dinginliği her zaman keyif verir. Metin çok değildir ama çok iş yapılır. Eski programlar derleyiciyle bir tartışma gibi değil, iyi konuşan bir araştırmacı ile iyi eğitilmiş bir makine meslektaşı arasındaki sessiz bir sohbet gibi okunur. İnceliğin bu gürültüyü satın alacağını kim bilebilirdi?” — Dick Gabriel
    • Öte yandan, yazılım mühendislerinin iyi bilmedikleri zaman soyutlamayı çok ileri götürdüklerine katılıyorum; ama meslek olarak yazılım mühendisi olmayanların yazdığı kodu da özellikle yüksek görmüyorum.
      İki uç noktayı da görüyoruz. Çok fazla soyutlama yüzünden aşırı dağılmış kod tabanları da, hiç soyutlama olmadığı için hedefe ulaşmaya yönelik script’lere benzeyen kod tabanları da çalışması zor şeyler.
      Python, JS, PHP script’lerinin çoğu “boş ver, bana istediğim sonucu ver yeter” tavrıyla yazılmıştı; her gün kodla çalışan insanların ise iş birliğini ve dayanıklılığı destekleyen soyutlamalara ihtiyacı var.
  • Başlardaki meme, iyileşmekte olan bir C programcısı olarak beni gerçekten güldürdü.
    Dilleri bu şekilde eğip bükerek kullanma örneklerini görmek eğlenceli; C’de bunun için bolca fırsat var, Go’da da görmek ilginç.

  • “C ve türevi dillerin egemen olduğu günlerden kalma eski bir programlama şakası” deniyor ama biz hâlâ o günlerde yaşıyoruz.

  • Bu thread’de bunun soyutlama fazlalığının tipik örneği diye eleştirilmesi ironik.
    C’de üç yıldızlı değişkenleri nadiren görmemizin nedeni pointer zincirlerinin nadir olması değil, aynı anda ikiden fazla seviyeyi manipüle etme ihtiyacının sık olmaması.
    Python, Java, JavaScript gibi çoğu şeyin varsayılan olarak pointer’a yakın olduğu dillerde, uzunluğu 4’ü çok aşan pointer zincirleri de kesinlikle var.
    Derin kısımlar genellikle içini dert etmeniz gerekmeyen struct’ların içinde gizlenir; yani soyutlanır.
    Kanallar da sıralı kodda pointer’ların oynadığı rolü eşzamanlı kodda kabaca oynadığından, burada ölümcül bir kusur varsa bu soyutlama fazlalığı değil, aksine soyutlama eksikliği olabilir.
    Yine de kod tabanını bilmiyorum; belki de chan chan, yazmak istedikleri şey için tam olarak doğru soyutlamaydı. Erlang gibi eşzamanlılığın güçlü olduğu dillerde, yanıtı alacak süreci tanımlamak için bir PID’yi başka bir PID’ye göndermek çok yaygındır.

    • Kariyerimin başlarında üç yıldızlı bir commit yüzünden azar işittiğimi hatırlıyorum. “Pointer’ın pointer’ının pointer’ına kim ihtiyaç duyar?” minvalindeydi.
      Ama mesele bu değildi; bu, bir string dizisinin adresiydi. C olduğu için böyleydi ve aradan yaklaşık 20 yıl geçtikten sonra bile bunun en sezgisel çözüm olduğunu düşünüyorum.
      Meslektaşımı savunmak gerekirse, muhtemelen yorum satırı yoktu. O kısım benim sorumluluğumdu.
  • Buena Vista Social Club’ın zamansız klasiği aklıma geliyor: https://www.youtube.com/watch?v=o5cELP06Mik

  • chan chan Value ya da chan struct{resp chan Value}, çok belirli durumlarda gerçekten kullandığım kalıplar
    Bir mesaj veri yolu da kullanabilirdim, ama o zaman bir de mesaj veri yoluyla uğraşmak gerekirdi

    • chan chan oldukça sık kullanılır. İç sunucuya ya da aktöre mesaj gönderirken, yanıtı almak için kullanılacak kanalı da mesaja ekleyip gönderme durumu
      Pratikte grep ile bulunabilecek birebir chan chan ifadesinden çok chan struct { ... içinde kanal olan bir şey ... } biçiminde olur, ama ilke aynı
      Çok kullanışlı bir kalıp ve Go’nun temel becerilerinden biri olduğunu düşünüyorum
      Ancak bildiğim kadarıyla chan chan chan hiç kullanmadım
      Bunların hepsini mesaj veri yoluna çevirmek aşırıya kaçmak olur. Performans özellikleri de çok farklıdır ve Go’nun kanalları işletim sistemi sürecinin iç bileşenlerine daha yakın olduğundan, süreç içi iletişim için bunu mesaj veri yoluna “yükseltmeye” değmez
    • Muhtemelen gopl’nin eşzamanlı sohbet sunucusundaki kalıba benzer. İlk başta biraz şaşırtıcı olsa da yine de oldukça okunabilir
      [0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
    • Ben de kullandım. Go’da Promise benzeri semantikler uygulamak için kullanışlı
  • Kanalın kanalı normal bir kalıptır; ancak genellikle daha sık, yapı değeri kanalı biçiminde ortaya çıkar ve bu yapı tipinin içinde bir kanal alanı bulunur
    Tamamlanacak istekler gönderip bu değerleri sıradan bağımsız beklemeyi sağlayarak başta bekleyen işin engellemesini önlemek için kullanılabilir
    Örneğin type request struct { params, reply chan response } gibi isteği kanala gönderir, worker işi işledikten sonra sonucu reply kanalına koyar
    Ama yararlı olduğu yer iki seviyeye kadardı; chan chan chan gerektiren bir kullanım durumu görmedim

  • Kanal gönderen bir kanalla dinamik dispatch uygulayan bir karşı örnek blog yazısı var. Go değil Limbo, ama kavram aynı. Belki karmaşıklığı tam da bu noktayı kanıtlıyordur https://ipn.caerwyn.com/2007/07/lab-78-dynamic-dispatch.html...

    • Merak edenler için ekleyeyim: Limbo, Go’nun öncüllerinden sayılabilecek bir dildi ve Plan 9’un ardılı Inferno işletim sisteminde kullanılıyordu
  • Joe Armstrong’un “My favorite Erlang Program” yazısı aklıma geliyor
    https://joearms.github.io/published/2013-11-21-My-favorite-e...