4-chan Go programcısı
(dolthub.com)- 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'nunchanyapısına uyarlayarak, en üst channel olarak_4chan:= make(chan chan chan chan int)kullanıyor factor = 3iken her channel katmanında producer ve consumer'lar dallanıyor, sonintdeğerleri toplanarak 3'ün 5. kuvveti olan 243 yazdırılıyor- Pratikte uygulama ve debug zorluğu, channel kapatma yönetimi,
sync.WaitGroupgereksinimi ve goroutine sızıntısı nedeniyle uygun değil; örnek, kapanış mantığı yerinetime.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,****intgeçirilir - Son fonksiyonda
****i = 100yapılırsa programi is now 100çıktısını verir
- Sırasıyla
- Go'da C'de olmayan
chanbulunduğ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
_4chanadı 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
factordeğerine göre producer oluşturuluyor- Örnekte
const factor = 3 sendChanChanChan, 3-channel producer'ı bir goroutine olarak başlatıyor
- Örnekte
- Consumer tarafı da her katmanda gelen channel'ı alıp bir sonraki katman için
factorkadar consumer başlatıyorreceiveChanChanChan,_4chanüzerinden_3chanalı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
intdeğerleri gönderiliyor sendfonksiyonu,_2chanüzerinden_1changönderip ardındanfactorkadar int producer başlatıyor- Her int producer da tekrar
factorkadar goroutine oluşturup_1chan <- 1işlemini yapıyor - Consumer, aldığı tam sayıları global
sumdeğişkenine ekliyorsum,atomic.Int32olarak tanımlıreceive(c chan int), channel'dan değer alıpsum.Add(int32(s))çalıştırıyor
Çalışma sonucu ve dallanma sayısı
- Tüm program
_4chanoluşturuyor, gönderim ve alım katmanlarını ayrı ayrı goroutine olarak başlatıyor, ardından500 * time.Millisecondboyunca 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
factorkullanmak istenirse, çalışmanın tamamlanabilmesi içinSleepsü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 kezstarting 2chan producer: 9 kezstarting 3chan consumer: 9 kezstarting 2chan consumer: 27 kezstarting chan producer: 27 kezstarting 1chan consumer: 81 kezstarting int producer: 81 kezsending int: 243 kezreceived 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.WaitGroupeklemek 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
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.
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.
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.
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.
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
İ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.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 Valueya dachan struct{resp chan Value}, çok belirli durumlarda gerçekten kullandığım kalıplarBir mesaj veri yolu da kullanabilirdim, ama o zaman bir de mesaj veri yoluyla uğraşmak gerekirdi
chan chanoldukç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 durumuPratikte grep ile bulunabilecek birebir
chan chanifadesinden çokchan 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 chanhiç kullanmadımBunları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
[0]: https://github.com/adonovan/gopl.io/blob/master/ch8/chat/cha...
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 sonucureplykanalına koyarAma yararlı olduğu yer iki seviyeye kadardı;
chan chan changerektiren bir kullanım durumu görmedimKanal 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...
Joe Armstrong’un “My favorite Erlang Program” yazısı aklıma geliyor
https://joearms.github.io/published/2013-11-21-My-favorite-e...