Golang - goroutine kullanma sanatı

4 minute read

Merhaba gönül dostları… Yepyeni bir konu ile, Golang’in can damarı goroutine’ler ile karşınızdayım. Öğrendikçe güncellemeyi düşündüğüm bu postta goroutineleri kullanırken dikkat edilmesi gereken şeylerden bahsedeceğim. Şimdilik niyetim bu fakat ileride konunun scope’unu daha da genişletme hakkını mahfuz tutuyorum.

Öncelikle en temelden başlayalım, goroutine nedir?

goroutineleri multithreading desteği olan diğer programlama dillerindeki threadlere benzetebiliriz. Aynı oradaki gibi bir çatallanma, bir eş zamanlılık (concurrency) söz konusu.

Şimdi kullanımıyla alakalı bazı durumlardan bahsetmeye başlayalım…

Döngü içerisinde goroutine kullanmak

Döngü içerisinde goroutine kullanırken çok dikkatli olmamız icab eden bazı hususlar var. Mazallah gözden kaçarsa programımızı göçertebilir. Daha da kötüsü, bu dikkat etmemiz gereken hususların neler olduğunu bilmezsek hataların neden kaynaklandığını çözmemiz de bir hayli zor olacaktır. Şimdi gelin o hususlara teker teker göz atalım.

goroutine fonksiyonuna parametre göndermek veya göndermemek,işte tüm mesele bu

Farazi anlatımla efkarınızı bulandırmadan somut bir örnek üzerinden gidelim. Şimdi https://play.golang.org/ adresine gidip aşağıdaki kodu çalıştıralım.

package main

import (
	"fmt"
	"sync"
)

func main() {
	Elements := [4]string{"bir", "iki", "üç", "dört"}
	var wg sync.WaitGroup
	for _, element := range Elements {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println(element)
		}()
	}
	wg.Wait()
}

Normalde Elements stringsi içerisindeki her elemanın ayrı ayrı goroutineler tarafından print edilmesini beklerken

dört
dört
dört
dört

şeklinde bir output aldık. Sebebi gayet basit.

Döngü içerisinde açtığımız goroutinelerde eğer döngünün her iterasyonunda değişen bir değişken kullanıyorsak (buradaki element stringi gibi) bu değişkeni goroutine’e parametre olarak vermemiz gerekir. Aksi taktirde oluşturduğumuz goroutineler bu parametrenin o anki değerini kullanacaktır. Oysa ki biz, her goroutine’in slice içerisindeki ayrı ayrı elemanları işlemesini istiyoruz. İşte bu sebeple iterasyonun o anki adımındaki değişkeni goroutine’e parametre olarak gönderiyoruz. Böylece goroutine o parametrenin döngüde değişen haline bağlı kalmamış oluyor.

Çalışan örneği görmek için şimdi aşağıdaki kodu çalıştıralım;

package main

import (
	"fmt"
	"sync"
)

func main() {
	Elements := [4]string{"bir", "iki", "üç", "dört"}
	var wg sync.WaitGroup
	for _, element := range Elements {
		wg.Add(1)
		go func(element string) {
			defer wg.Done()
			fmt.Println(element)
		}(element)
	}
	wg.Wait()
}

Görüldüğü gibi her goroutine ayrı bir elementi ekrana bastı.

Aynı anda çalışacak goroutine sayısını limitlemek

Bir döngüdeki her iterasyon için ayrı ayrı API isteği yapmamız gereken bir durum düşünelim. Örneğin totalde 1000 iterasyonluk bir döngümüz olsun. Eğer bu istekleri goroutine kullanmadan tek goroutine üzerinden yaparsak ciddi vakit kaybı yaşayacağımız aşikar. Bu yüzden API çağrılarını goroutinelerle eş zamanlı olarak yapmak istiyoruz. Zaten Go’yu biraz da bu işleri hızlıca yapabilmek için kullanıyoruz.

Burada ortaya şöyle bir problem çıkıyor, örneğimizdeki gibi 1000 iterasyonluk bir döngüde her iterasyonda ayrı bir goroutine açmak belki sistemsel olarak bizi sıkıntıya sokmaz fakat kullandığımız API servisinin limitasyonlarına takılabiliriz. API servisi bir kullanıcının eş zamanlı olarak belirli sayıda istek yapmasına izin veriyor olabilir veya böyle bir kısıtlama olmasa dahi eş zamanlı olarak 1000 istek gönderdiğimizde sunucuları bunu handle edemediğinden bize hata dönebilir. Bu tür komplikasyonların önüne geçmek için eş zamanlı çalışacak goroutinelerin sayısını limitlemekte fayda var. Peki bunu nasıl yapıyoruz? Efendim gayet basit;

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"sync"
)

func main() {

	var wg sync.WaitGroup
	limiter := make(chan struct{}, 10)
	for i := 1; i <= 200; i++ {
		limiter <- struct{}{}
		wg.Add(1)
		go func(i int) {
			defer func() { <-limiter }()
			defer wg.Done()
			url := fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%d", i)
			resp, err := http.Get(url)
			if err != nil {
				fmt.Printf("%s", err)
			}
			defer resp.Body.Close()
			body, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				fmt.Printf("%s", err)
			}
			fmt.Println(string(body))
		}(i)
	}
	wg.Wait()
}

Yukarıdaki kodu Go Playground program içerisinde http isteği yapmaya izin vermediği için lokalimizde çalıştırmamız gerekiyor. Ben biraz önce denedim, çalışıyor sorun yok yani:) Ama sonucu kendiniz de görmeniz faydanıza olacaktır.

Evet burada ne yaptık? Öncelikle ilk örneğimizle aynı şekilde main goroutine’imizin yavrucuklarını beklemeden execute olmaması için WaitGroup tanımladık. Ardından döngüde kullanacağımız i indeksini oluşturduğumuz goroutinelerimize parametre olarak geçtik. Bu indeksi goroutinelerimiz içerisindeki fake API url’ine ekleyerek isteğimizi yaptık. Sonrasında ise aldığımız cevabı ekrana bastık.

Burada bizi asıl alakadar eden kısım limiter tanımlaması. Dikkat ederseniz, WaitGroup tanımlaması altındaki satırda limiter := make(chan struct{}, 10) şeklinde bir limiter channel’ı tanımladık. Bu satırda söylediğimiz şeyin türkçesi şu “Boyutu 10 olan ve içerisinde struct{} (boş struct) tipinden değişkenler tutacak bir channel tanımla”.

Burada konuyla alakalı tecrübemiz yoksa akla channel nedir? sorusu gelebilir. Bu aslında müstakil bir post olarak işlenebilecek bir konu fakat kısaca anlatmak gerekirse channellar goroutineler tarafından ortak kullanılan shared memoryler olarak düşünülebilir. İsminin yaptığı çağrışımı baz alarak “goroutineleri birbirine bağlayan kanallar” da diyebiliriz.

channel’lar yukarıda gösterildiği gibi initialize edilir. İçerisinde hangi tipte veri barındıracağı da bu initialization sırasında belirtilmelidir. (interface{} tipi bahisten hariçtir.)

channel’lara veri atar ve çekeriz. Zaten kullanım amacı da malumunuz olduğu üzere budur. Burada Go’nun bir güzelliği de şudur ki, bir goroutine channel’a veri atmak isterse ve channel doluysa bu goroutin o channel’da bir yer boşalana dek sabırla, sadakatle o satırda bekler. Ne zaman ki channel’da bir boşluk olur, bekleyen goroutinemiz veriyi channel’e atıp yoluna devam eder.

Bu durum bize istediğimiz zaman goroutineleri bekletebilme gibi bir lüks sağlar. Aslında yukarıda yaptığımız da tam olarak budur.

limiter <- struct{}{} satırında limiter channel’ımıza boş bir struct atıyoruz.

defer func() { <-limiter }() satırıyla ise her goroutine, bitiminde limiter’dan veri çekiyor.

limiter channelımızın kapasitesini 10 olarak belirlediğimiz malum, o halde 11. boş structu limiter’a atmak isteyen goroutine’miz ne yapar? Evet dostlar, bekler. Yani o anda çalışan goroutinelerden birisi defer func() { <-limiter }() satırıyla limiter içerisinden veri çekip execute olduğu an sırada bekleyen goroutine çalışmaya başlar. Böylece her seferinde aynı anda 10 goroutine çalışacaktır.

Görüldüğü üzere eş zamanlı olarak çalışacak goroutine sayısını böylece limitlemiş olduk.

Leave a comment