Goroutine Temelleri
Goroutine
Gopher'lara golang'in avantajları sorulduğunda sıklıkla öne çıkan bir konu eşzamanlılık (Concurrency) ile ilgili yazılardır. Bu konunun temelini hafif ve kolayca işlenebilen goroutine'ler oluşturur. Bu makalede bu konuyu kısaca ele aldım.
Eşzamanlılık (Concurrency) vs. Paralellik (Parallelism)
Goroutine'i anlamadan önce, sıklıkla karıştırılan iki kavramı açıklığa kavuşturmak isterim.
- Eşzamanlılık: Eşzamanlılık, birçok işi aynı anda ele almakla ilgilidir. Bu, işlerin fiilen aynı anda yürütüldüğü anlamına gelmez; aksine, birden fazla görevin küçük birimlere ayrılması ve dönüşümlü olarak yürütülmesi yoluyla, kullanıcının bakış açısından birçok görevin aynı anda işleniyormuş gibi görünmesini sağlayan yapısal ve mantıksal bir kavramdır. Tek çekirdekli sistemlerde bile eşzamanlılık mümkündür.
- Paralellik: Paralellik, "birden fazla çekirdekte birden fazla işi aynı anda işlemek"tir. Kelimenin tam anlamıyla işlerin paralel olarak ilerlemesi ve farklı görevlerin eşzamanlı olarak yürütülmesidir.
Goroutine, Go runtime scheduler aracılığıyla eşzamanlılığı kolayca uygulamayı sağlar ve GOMAXPROCS ayarı aracılığıyla paralelliği doğal olarak kullanır.
Sıklıkla kullanılan Java'nın Multi-thread yapısı, paralelliğin tipik bir örneğidir.
Goroutine Neden İyidir?
Hafiftir (lightweight)
Oluşturma maliyeti diğer dillere göre oldukça düşüktür. Burada akla "golang neden az kaynak kullanır?" sorusu gelir, çünkü oluşturma yeri Go runtime içinde yönetilir. Bunun nedeni yukarıda bahsedilen hafif mantıksal thread olmasıdır. OS thread biriminden daha küçüktür ve başlangıç stack'i yaklaşık 2KB boyutunda bir alan gerektirir ve kullanıcının uygulamasına bağlı olarak stack eklenerek dinamik olarak değişebilir.
Stack birimiyle yönetildiği için oluşturma ve kaldırma işlemleri çok hızlı ve düşük maliyetli olup, milyonlarca goroutine çalıştırılsa bile yük oluşturmayacak bir işlem kapasitesi sağlar. Bu sayede Goroutine, runtime scheduler sayesinde OS kernel müdahalesini minimize edebilir.
Performanslıdır (performance)
Öncelikle Goroutine, yukarıdaki açıklamada belirtildiği gibi OS kernel müdahalesi az olduğu için User-Level'da context switching yaparken OS thread birimine göre daha düşük maliyetle hızlı bir şekilde görevleri değiştirebilir.
Buna ek olarak, M:N modelini kullanarak OS thread'lerine tahsis edilerek yönetilir. Bir OS thread havuzu oluşturularak, birçok thread'e ihtiyaç duymadan az sayıda thread ile işlem yapılabilir. Örneğin, bir sistem çağrısı gibi bir bekleme durumuna düşüldüğünde, Go runtime, OS thread'inde başka bir goroutine çalıştırarak OS thread'inin boş durmamasını ve CPU'yu verimli bir şekilde kullanarak hızlı işlem yapılmasını sağlar.
Bu durum, Golang'in özellikle I/O işlemlerinde diğer dillere göre daha yüksek performans sergilemesini sağlar.
Sade (concise)
Eşzamanlılık gerektiğinde, go anahtar kelimesiyle bir fonksiyonu kolayca işleyebilmek de büyük bir avantajdır.
Mutex, Semaphore gibi karmaşık Lock'lar kullanılması gerektiğinde ve Lock kullanıldığında kaçınılmaz olarak DeadLock durumu göz önünde bulundurulması gerektiğinde, geliştirme öncesi tasarım aşamasından itibaren karmaşık adımlar gerekmektedir.
Goroutine, "belleği paylaşarak iletişim kurmayın, iletişim kurarak belleği paylaşın" felsefesine uygun olarak Channel aracılığıyla veri iletimini teşvik eder ve SELECT ise Channel ile birleşerek verinin hazır olduğu kanaldan işlem yapma özelliğini destekler. Ayrıca, sync.WaitGroup kullanıldığında, tüm goroutine'lerin bitmesini basitçe bekleyerek iş akışını kolayca yönetmek mümkündür. Bu araçlar sayesinde thread'ler arasındaki veri yarışması sorunları önlenir ve daha güvenli eşzamanlılık işleme sağlanır.
Ayrıca, context kullanarak kullanıcı düzeyinde (User-Level) yaşam döngüsü, iptal, zaman aşımı, son teslim tarihi ve istek kapsamını kontrol edebilme imkanı, belirli bir düzeyde istikrarı garanti eder.
Goroutine'in Paralel İşlemleri (GOMAXPROCS)
Goroutine'in eşzamanlılıkta iyi olduğu belirtilmesine rağmen, "paralelliği desteklemiyor mu?" şeklinde bir soru akla gelebilir. Son zamanlarda CPU çekirdeklerinin sayısı eskiden farklı olarak iki haneyi aşmış durumda ve ev bilgisayarlarında da azımsanmayacak sayıda çekirdek bulunmaktadır.
Ancak Goroutine paralel işlemleri de yürütür; bu GOMAXPROCS'tur.
GOMAXPROCS ayarlanmadığında, sürüme göre farklı şekilde yapılandırılır.
1.5 öncesi: Varsayılan değer 1'dir; 1'den fazlası gerektiğinde
runtime.GOMAXPOCS(runtime.NumCPU())gibi bir yöntemle ayar yapılması zorunludur.1.5 ~ 1.24: Kullanılabilir tüm mantıksal çekirdek sayısına değiştirilmiştir. Bu zamandan itibaren geliştiricinin büyük ölçüde bir kısıtlama gerektirmeyen durumlarda ayar yapmasına gerek kalmamıştır.
1.25: Konteyner ortamlarında popüler bir dil olarak, Linux üzerindeki cGroup'u kontrol ederek konteynerde ayarlanmış
CPU kısıtlamasınıdoğrular.Dolayısıyla, mantıksal çekirdek sayısı 10 ve CPU kısıtlama değeri 5 ise,
GOMAXPROCSdaha düşük olan 5 olarak ayarlanır.
1.25 sürümündeki bu değişiklik oldukça büyük bir etkiye sahiptir. Zira konteyner ortamlarındaki dil kullanım oranı artmıştır. Bu sayede gereksiz thread oluşturma ve context switching azaltılarak CPU throttling'in önüne geçilmiştir.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Başlangıç
14 time.Sleep(10 * time.Millisecond) // 작업 시뮬레이션을 위한 지연 // İş simülasyonu için gecikme
15 fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Başlangıç
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // CPU 코어 2개만 사용 // Sadece 2 CPU çekirdeği kullan
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Tüm goroutine'ler bitene kadar bekleniyor...
29 wg.Wait()
30 fmt.Println("모든 작업이 완료되었습니다.") // Tüm görevler tamamlandı.
31
32}
33
Goroutine'in Zamanlayıcısı (M:N Modeli)
Önceki bölümde bahsedilen M:N modelini kullanarak OS thread'lerine tahsis edilerek yönetilir kısmına biraz daha ayrıntılı girersek, goroutine GMP modeli vardır.
- G (Goroutine): Go'da yürütülen en küçük iş birimi
- M (Machine): OS thread (gerçek işin yapıldığı yer)
- P (Processor): Go runtime tarafından yönetilen mantıksal işlemci
P ayrıca yerel bir yürütme kuyruğuna (Local Run Queue) sahiptir ve atanmış G'leri M'ye atayan bir zamanlayıcı görevi görür. Basitçe goroutine,
GMP'nin çalışma süreci aşağıdaki gibidir:
- G (Goroutine) oluşturulduğunda, P (Processor)'nin yerel yürütme kuyruğuna atanır.
- P (Processor), yerel yürütme kuyruğundaki G (Goroutine)'yi M (Machine)'ye atar.
- M (Machine), G (Goroutine)'nin block, complete, preempted durumunu döndürür.
- Work-Stealing (İş Çalma): Eğer P'nin yerel yürütme kuyruğu boşalırsa, başka bir P global kuyruğu kontrol eder. Orada da G (Goroutine) yoksa, başka bir yerel P (Processor)'nin işini çalarak tüm M'lerin boş durmadan çalışmasını sağlar.
- Sistem Çağrısı İşleme (Blocking): G (Goroutine) çalışırken bir Block durumu oluşursa, M (Machine) bekleme durumuna geçer. Bu durumda P (Processor), Block olan M (Machine)'den ayrılarak başka bir M (Machine) ile birleşir ve bir sonraki G (Goroutine)'yi çalıştırır. Bu sayede I/O işlemi sırasında bekleme süresinde bile CPU israfı olmaz.
- Bir G (Goroutine) uzun süre öncelik (preempted) alırsa, diğer G (Goroutine)'lere yürütme fırsatı verir.
Golang'in GC (Garbage Collector)'si de Goroutine üzerinde çalışır, bu sayede uygulamanın yürütülmesini minimum düzeyde kesintiye uğratarak (STW) belleği paralel olarak temizleyebilir ve sistem kaynaklarını verimli bir şekilde kullanır.
Son olarak, Golang dilin güçlü avantajlarından biridir ve bunun dışında da birçok avantajı vardır; umarım birçok geliştirici Golang'den keyif alır.
Teşekkür ederim.