- read

Concurrency Nedir, Yenir mi? Go ile Concurrency

Ali GÖREN 13

Selamlar. Bu yazıda size concurrency hakkında ufak bilgileri go dilini kullanarak sunmaya çalışacağım. Eksik, hatalı noktalarım olabilir bu konuda da okuyucuların affına sığınmak isterim.

Nedir Bu Concurrency?

Concurrency, kısacası birden fazla işi eş zamanlı olarak gerçekleştirmek demektir. Varsayalım çalışırken kapı çaldı. Gidip kapıyı açıp işinizi tamamlayıp tekrar işinizin başına dönerseniz bu bir concurrency örneği olabilir. Bu noktada concurrency ile parallelism ayırt edilebilir oluyor. Yani kişinin birden fazla şeyle baş edebilmesini concurrency olarak görmek doğru olabilir.

Yukarıdaki görsel için kusura bakmayın. Diagram çizme işini pek beceremiyorum 🤣

Concurrency ve Parallelism Arasındaki Fark Nedir?

Parallelism ise aynı anda birden fazla işi yapmak gibi düşünülebilir. Hmm bunu nasıl örneklesek? Beyninizin 4 farklı slotu olduğunu düşünün. Bu slotlar da A, B, C ve D işlerine sahip olsun. Bunların hepsi aynı anda çalışsın. İşte paralel çalışma tam olarak böyle düşünülebilir.

İkincil örneği beyninizdeki her bir slot ya da core 1, core 2, core 3 ve core 4 olarak düşünebilirsiniz. Bunların hepsi aynı anda çalışan işlerdir.

Peki şunu diyebilirsiniz, birisinde single core var ve aksiyonlar sırayla gidiyor gibi, ancak parallelismde işler bölünmüş. Niye parallelism kullanmayalım? Burada parallelism her zaman hızlı olmayabiliyor. Çünkü bu işler arası iletişim, bazen beklediğimiz hızı bize sunmayabiliyor. Burada çekirdekler arasındaki iletişim yükü yüksek ise, davranış olarak da hızlı sonuçlar bize gelmeyebiliyor.

Go, Concurrency ve Parallelism

Go, parallelism desteklemeyen bir dil. Go bu tarz problemleri concurrency tabanında çözüme ulaştırmayı hedeflemekte.

Goroutine Kavramı

Go dilinde concurrent uygulamaları goroutine dediğimiz yapılarla gerçekleştirebiliyoruz. Goroutine’ler ise fonksiyonlar ya da methodlar olabiliyor.

Bir goroutine’i, thread’in hafif versiyonu olarak düşünebilirsiniz. Her defasında yeni bir thread oluşturmaktansa goroutine açmanın maliyeti daha iyidir. Goroutine’ler stack’te saklanan ufak boyutlu yapılardır. Stack bildiğiniz gibi ihtiyaca göre büyüyüp küçülebilir. Thread’ler ise fixed size’a sahip olmalı bu nedenle birazcık bizi mutsuz ediyor 😒

Goroutine’lerde iletişim görevini channels dediğimiz yapılar sağlıyor. Bu yazı biraz uzayacağı için çok bahsetmeyeceğim. Ancak enerjim olursa sonraki yazıda bunlara değineceğim.

İlk Gorutine’imizi Yazalım

Goroutine oluşturmak için fonksiyon çağrılarının önüne go keyword’ünü koyarız. Böyle de narsist bir dil he.

Yukarıdaki örnekte ilk concurrenct çağrımızı yaptık. Ancak bu kodu çalıştırırsak hiçbir çıktı elde edemeyeceğiz. Niye? Çünkü goroutine işlemleri, fonksiyonları beklemek zorunda değildir.

Yani bir goroutine çağırısı yapıldığı anda anında yani geri dönüş işlemi olur. Fakat siz bu fonksiyonun çalışmasını beklemek zorundasınız. Bu fonksiyon henüz çalışmadan return olduysa haliyle içindeki ifadeyi de çalıştıramazsınız.

Buradan anladığımız, ornek_mesaj çağrısını gerçekleştirdiğiniz anda henüz daha fonksiyon çalışmadan alt satıra yani main fonksiyonunun tamamlandığı noktaya geçiş yapılır. Bunu engellemenin bir yolu, ilgili çağrının çalışacağı süre kadar sleep uygulamak olabilir. Bu sevilen bir yol değildir bu arada. Ancak örneklemek isterim.

1 saniyelik bir beklemenin ardından concurrent işlemimizin gerçekleştiğini çıktıdan görebiliriz. Bu örnekte 2 farklı çağrının sonucunun aynı olduğunu görebiliriz.

Doların 1,5 TL olması durumu var. Varsa dolarınız satın! index: 1, time: 2022-05-19 17:19:27.1966317 +0300 +03 m=+0.00155270Doların 1,5 TL olması durumu var. Varsa dolarınız satın! index: 2, time: 2022-05-19 17:19:27.1966317 +0300 +03 m=+0.0015527011

Yukarıdaki çıktı bize örnek olabilir. Farklı indexler, bize aynı sonuçları döndürüyor. İsterseniz go keywordünü kaldırarak farkı görebilirsiniz.

Basit Bir Goroutine Uygulaması Yapalım

Diyelimki websitelerine istek atıp bunların title’larını çeken bir uygulama yapıyoruz. Bu işlemi önce normal şekilde, sıralı bir biçimde alacak, haliyle her bir response’un sonucunu da alacak biçimiyle yaptığımızı varsayalım. Kullanılan paket çok önemli olmasa da title’ı almak için goquery paketini kullandım.

Concurrent Olmayan Yapıda HTTP Çağrıları

Gist: https://gist.github.com/aligoren/1f05354f5ee458e4bde2d459c4f9a1dd

Yukarıdaki örnekte concurrent olmayan bir yapıda HTTP çağrıları gerçekleştirdik. Gelin bu kodun çalışma zamanına bakalım

Sonuç ne düzeyde görünüyor bilemesem de yaklaşık 2.5 saniye alan bir sonuç üretti. Şimdi bu kodu concurrent yapıya çevirelim.

Concurrent HTTP Çağrıları

Çok basit 2–3 değişiklikle bu kodu concurrent yapıya çevirdik.

Gist: https://gist.github.com/aligoren/32d040f803f3828dab7c39ba6a08664a

Şimdi yukarıdaki koda baktığımızda WaitGroup kullanımını görmekteyiz. Bu, birden fazla goroutine’in bitmesini beklediğimiz durumlarda kullanabileceğimiz bir yapıdır.

WaitGroup, 0'a gelene kadar yani bütün taskler done olana kadar bütün goroutine çağrılarının çalışmasını bekler. Hepsi tamamlanınca da kod bir sonraki satıra geçiş yapar.

Bu kod şu anda tutarlı çalışan bir kod. Ancak eğer mutex kullanmamış olsaydık, data race oluşacaktı. mu değişkeni ve ona bağlı fonksiyon çağrılarını yorum satırına alıp aşağıdaki komutu verelim.

go run -race .\main.go

Ups! Data race’ler bize görünüyor. Data race kısacası paylaşılan bir bellekteki veriye birden fazla resource’un ulaşıp veri yazması oluyor. Burada problem, bir kaynak, öteki kaynağın verisini değiştirebilir ve bu nedenle diğer kaynağın verisi kaybolabilir. Burada bizi bekleyen potansiyel tehlike, array’in içerisine bir verinin yazılmaması ya da yazılıyor ise de yanlış verinin yazılması olabilirdi. Mesela Ekşi Sözlük için atılan istekte, title verisinin github’a ait olması gibi. Bunu engellemek için, data güncellenecek noktadan önce şöyle bir yapı kullanabiliriz.

mu.Lock()
responses = append(responses, response)
mu.Unlock()

Mutext burada locking mekanizması çalıştıran bir yapıdır. Buna sonra değinebiliriz. Ancak, burada verinin güvenli bir şekilde değiştirildiğinden emin olabiliriz.

Konuyu uzatmadan asıl konumuza dönelim. Diyorduk ki, concurrent HTTP istekleri bize daha hızlı sonuçlar dönecektir. Deneyelim?

Sonuca göre 750 ms içerisinde bu çağrıları sonuçlandırdık. Bu tarz işlemleri concurrency olmadan da yapabiliriz ancak çok daha uzun süreler alabilir.

Bu yazıda anlatmak istediklerim bu kadar. Umarım faydalı bir yazı olmuştur.

Okuduğunuz için teşekkür ederim :)

Kaynaklar