Goroutine ve Go Scheduler Rehberi Part 1

Mehmet Can Tas
7 min readFeb 16, 2022

Herkese merhaba,

Bugün uzun zamandır araştırdığım konular olan Goroutine ve Go Scheduler hakkında bildiklerimi sizlere aktarmak istiyorum. Makale çok uzun olacağından dolayı 2 (belki 3) parça halinde paylaşacağım.Bu kadarlık giriş sohbetinden sonra hadi başlayalım.

Temel bilgiler

Goroutine’in ne olduğunu ya da neden bu tarz bir şeye ihtiyacımız olduğunu anlamamız için yerine kullanabileceğimiz şeyleri artı ve eksi yönleriyle anlamamız gerektiğini düşünüyorum. Bu sebepten dolayı temel bilgilerden başlayıp ileri seviye bilgilere doğru gideceğiz.

Concurrency nedir?

Çoğunuz “concurrency nedir?” sorusuna verilen cevapları bir şekilde duymuşsunuzdur. En popüler cevaplardan birisi “concurrency, birden fazla iş ile aynı anda ilgilenebilme becerisidir” diyebiliriz. Bu cümleyi hem teknik olarak hem de günlük hayattan örneklerle açıklamaya çalışayım.

Bir programı çalıştırdığınızda öncelikle belleğe yüklenir ve bir process olarak nitelendirilir. Process’ler kendi içlerinde bir ya da birden fazla Thread barındırabilirler.

Thread dediğimiz şey ise, uygulamamızın çalışan bir parçası anlamına gelmektedir. Örneğin “maaş hesaplama fonksiyonumuz bir thread üzerinde çalışır”, “veri tabanına sorgu gönderdiğimiz fonksiyon bir thread üzerinde çalışır” gibi. Her bir thread T anında sadece bir iş yapabilme becerisine sahiptir. Modern bilgisayarlarda CPU (Central Processing Unit) içerisinde birden fazla core (çekirdek) bulunur. Process içerisinde bulunan bu Thread’ler bir CPU çekirdeği üzerinde çalışırlar. Her bir core aynı anda sadece bir Thread çalıştırabilir. Bir core’a atanmış birden fazla thread olabilir ve aslında bu noktada yukarıda bahsettiğimiz “aynı anda birden fazla iş ile ilgilenebilme” tanımına da varmış oluyoruz. Elbette CPU çekirdeğimizden daha fazla thread oluşturulabilir.

Şu an bilgisayarınızda görev yöneticisi’ni açıp baktığınız zaman binlerce thread olduğunuz görebilirsiniz. Aşağıdaki ekran görüntüsünde de görebileceğiniz gibi 6 çekirdek olmasına rağmen toplamda thread sayısı 6248.

CPU her bir Thread arasında geçiş yaparak Thread’lerin çalıştırılmasını sağlamaktadır. Bu işleme context-switch adı verilir. Bir Thread I/O işlemi yaptığında CPU Thread’in o ank itüm bilgilerini kaydeder ve o Thread işlemini bitirene kadar bir köşeye alır. Onun yerine ise çalıştırılmaya hazır olan diğer Thread’i çalıştırmaya başlar. Köşeye alınan thread işlemini bitirdiğinde ise kuyruğun en sonuna geçerek çalıştırılmaya hazır bir şekilde sırasının gelmesini bekler.

Asenkron programlama ve multi-threading kavramları ise concurrency kavramının formlarındandır.

Günlük hayat örneği olarak ise hepimiz mesai saatimizde bir proje geliştiriyoruz. Bu projeyi geliştirirken Scrum, Kanban vb. yöntemleri kullanıyoruz. Örneğin, siz bir sprint içerisinde bir işe başladınız ve bu iş için bir branch oluşturdunuz. Geliştirmeye devam ederken production ya da test ortamında bir hata oluştu ve o hatayı çözmeniz gerektiğini düşünün. O esnada çalıştığınız branch’de yazdığınız kodları commit’leyip kaydediyorsunuz ve master/hotfix/fix branch’ine geçiş yapıyorsunuz. Yaşanan hatayı çözdükten sonra da tekrar çalıştığınız branch’e dönüp kaldığınız yerden devam ediyorsunuz. Bu örnek hem context-switch hem de concurrency kavramlarını daha net ve açıklayıcı bir şekilde anlamınızı sağlayacaktır diye düşünüyorum.

Asenkron programlama ile ilgili bilgi için : https://medium.com/software-development-turkey/asenkron-programlama-hakk%C4%B1nda-bilinmesi-gerekenler-kavramlar-694a990efe86

Paralellism nedir?

Paralellism de concurrency kavramının bir başka şeklidir. Concurrency’nin aksine, birden fazla işi aynı anda gerçekleştirme becerisidir. Az önce verdiğim ekran görüntüsünde 6 core olduğundan bahsetmiştik. Bu da aynı anda 6 farklı işin gerçekleştirilebileceği anlamına geliyor. Bu işler birbiriyle alakalı olabilir ya da olmayabilir. Paralellism için birden fazla işlemci ya da bir işlemci üzerinde birden fazla çekirdeğe (core) sahip olmanız gerekli. Tek CPU ve tek çekirdekli bir makinede paralel kod çalıştıramazsınız.

Burada dikkat edilmesi gereken nokta ise, concurrency, yazdığınız kod ve projenizin tasarımı ile alakalı bir kavramdır. Paralellism ise uygulamanızın çalışma anı ile alakalıdır. Paralel çalışacak bir kod yazamazsınız, kodunuz bu özelliğe sadece çalışma zamanında sahip olur. Kodunuzdan bahsederken concurrency’den bahsedebilirsiniz sadece. Concurrency, çalışma anında gerçekleşecek paralellism’e kapı aralar. Bundan dolayı, concurrency yapısı iyi tasarlanmış bir projenin paralellism konusunda sizlere iyi bir altyapı oluşturduğunu söyleyebilirim.

Concurrency is a property of the code; parallelism is a property of the running program.

Concurrency In Go / Kathrine Cox-Buday

Kısaca ‘Thread’ nedir?

  • Thread, işletim sistemi zamanlayıcısı (scheduler) tarafından yönetilen, programımızın çalıştırılabilir bir parçasıdır.
  • Windows bilgisayarlarda bir ayar yapılmadığında varsayılan Thread stack bellek boyutu 1 MB olarak belirlenir.
  • Bazı kaynaklarda light-weight process olarak tanımlanır.
  • User Thread ve kernel Thread olarak iki farklı Thread tipi mevcuttur.

Thread ile alakalı daha detaylı bilgi için : https://devnot.com/2021/thread-nedir-detayli-bir-thread-incelemesi/

Go Concurrency Temelleri ve Communicating Sequential Processes

Go dokümanlarında da sıkça karşımıza çıkan “Do not communicate by sharing memory; instead, share memory by communicating.” cümlesini en azından bir kere görmüşsünüzdür. Aslında bu cümle ve Go dilinin concurrency modeli 1978 yılında C.A.R. Hoare tarafından yayımlanan Communicating Sequantial Processes (CSP) başlıklı bir akademik makaleye dayanmaktadır.

CSP sıralı (sequential) işlemlerin birbirleri ile channel’lar vasıtasıyla haberleşme yöntemlerini matematiksel ifadelerle tanımlar.

CSP, temelinde I/O komutlarınından oluşur. Bir işlemin çıktısı başka bir işlemin girdisi olmak zorunda ve bir process’in sahip olduğu veriye başka birinin erişmemesi gerektiğini belirtir.

Konuyu daha da basitleştirecek olursak, bu tıpkı günlük hayatta yaptığımız WhatsApp üzerinden mesajlaşma gibi. Sizin mesaj gönderme aksiyonunuzun çıktısı karşı taraf için mesaj okuma işleminin girdisi oluyor ve siz mesajı okuyan taraf olarak mesaj size gelmediği sürece o mesaja erişemiyorsunuz.

Concurrency modellemesi CSP’e dayanan çok fazla dil geliştirilmiştir. Bunlardan bazıları; Occam, Newsqueak, FortranM, Go, Clojure

CSP, eğer iki concurrent process arasında bir veri paylaşımı yapılması gerekiyorsa bunun channel’lar ile yapılması gerektiğini belirtir. Channel aslında en basit haliyle bir tarafın içerisine mesaj atıp diğer tarafın o mesajı alıp okuduğu bir tüp olarak düşünülebilir. Bu makalede Go’da channel yapısına çok değinmeyeceğim ancak merak edenler aşağıdaki videoya göz atabilirler.

More ideas in this paper than in any other 10 good papers you are likely to find

-Rob Pike

Goroutine

Bu kadar temel bilgiden sonra 2 ana konumuzdan ilkine geçebiliriz.

Goroutine’ler çoğu makalede light-weight thread olarak adlandırılır. Go runtime’a özel olan ve Go runtime içerisinde yer alan Go scheduler tarafından yönetilen yapılardır. Günün sonunda Goroutine tıpkı Thread gibi uygulamamızın çalışan bir parçasıdır.

Elbette Goroutine’lerin Thread’lere göre farklılıkları mevcut bunları sıralayacak olursak;

  • Goroutine’ler Thread’lere göre daha az bilgi barındırdığından context-switch işleminde oluşan maliyet daha azdır.
  • Go 1.2 sürümünde Goroutine’lerin stack boyutu 4kB iken Go 1.4 sürümünde 8kB şu an için sadece 2kB’dır.
  • Goroutine’ler Go runtime içerisinde yer alan Go scheduler tarafından yönetilir.
  • Goroutine’ler kendi stack boyutlarını dinamik olarak çalışma anında büyütüp tekrar küçültebilirler.
  • Goroutine’ler işletim sistemi Thread’leri üzerinde çalışan yapılardır.

Goroutine’lerin context-switch maliyetlerinin düşük olmasının temel sebebi ise Thread’ler preemptive olarak schdule edilirken Goroutine’le cooperative olarak schedule edilir. (Scheduler kısmında bu konulara değineceğim)

Thread’ler preemptive olarak schedule edildiği için bir CPU, Thread’leri durdurup tekrar çalıştırılırken birçok bilgiyi kaydedip tekrar yükler. Örnek olarak context-switch esnasında Thread’in kaydedilen bilgilerini görebilirsiniz.

16 general purpose registers, PC (Program Counter), SP (Stack Pointer), segment registers, 16 XMM registers, FP coprocessor state, 16 AVX registers, all MSRs etc.

Goroutine’lerde ise bu sayı yalnızca 3'tür;

Program Counter, Stack Pointer ve DX

Her Go projesinde main Goroutine adı verilen bir Goroutine bulunur. Bunu main Thread olarak düşünebilirsiniz. Fonksiyonunuzun farklı bir Goroutine üzerinde çalışmasını istiyorsanız önüne go anahtar kelimesini eklemeniz yeterli olacaktır.

func main(){
go myAnotherFunc()
}

Eğer bir Goroutine’i manuel olarak sonlandırmak isterseniz aşağıdaki gibi runtime.GoExit() metodunu kullanabilirsiniz.

func main(){    
go func(){
runtime.GoExit()
fmt.Println("Hello,World")
}
}

Go dilinin kaynak kodunda Goroutine’ler “g” adında bir struct ile tanımlanır.

type g struct {stack       stack   // offset known to runtime/cgom         *m      // current m; operating system threadsched     gobufsyscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gcsyscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gcstktopsp  uintptr // expected sp at top of stack, to check in tracebackatomicstatus uint32waitsince    int64      // approx time when the g become blockedwaitreason   waitReason // if status==GwaitingactiveStackChans boolparkingOnChan uint8raceignore     int8     // ignore race detection events}

Goroutine Stack İmplementasyonu

Goroutine’ler 2kB ile hayatlarına başlayıp çalışma zamanında ihtiyaca göre kendi stack boyutlarını arttırıp tekrar azaltabiliyorlar. Bu işlemin nasıl gerçekleştiğine daha yakından bakalım

Split Stack

Goroutine’ler üzerinde çeşitli implementasyonlar denendi bunlardan birisi de Split Stack implementasyonuydu.

Bu implementasyonda eğer fonksiyonunuz 2kB’dan fazla stack boyutuna ihtiyaç duyuyorsa yeni bir stack segment daha allocate edilip fonksiyonunuz ve stack pointer o segmente taşınıyordu. Aşağıdaki görselde çalışma şeklini görebilirsiniz

Bu implementasyon 32-bit işlemcilerde 1 milyon Goroutine oluşturmanızı sağlıyordu. Ancak bazı sıkıntıları da beraberinde getiriyordu.

Bunlardan ilki stabil olmayan performans sonuçlarıydı. Örneğin normal çalışan bir fonksiyonunuzda bir bugfix yaptığınızda ya da yeni bir şey ekleyip tekrar production ortamına gönderdiğinizde daha yavaş çalışabiliyordu. Bunun sebeplerinden en basiti ise normal bir fonksiyon çağrısı 2ns iken Split Stack implementasyonunda bu çağrı süresi 60ns olabiliyordu. Bu da stabilite konusunda sorunlara yol açıyordu.

Growable Stack

Farklı bir yöntem olarak Growable Stack implementasyonu kullanıldı. Temelinde yaptığı işlem şu çalıştırdığınız fonksiyon eğer 2kB’dan daha fazla stack boyutuna ihtiyaç duyuyorsa daha büyük bir stack segment allocate ediliyor ve eski allocate edilmiş stack içerisindeki bilgiler yenisine kopyalanarak eskisi serbest bırakılıyor. Aşağıdaki görselde bu implementasyonun çalışma şeklini sıralı bir şekilde görebilirsiniz.

Elbette hiçbir şey kusursuz olmadığı gibi bu implementasyonun da barındırdığı farklı sorunlar mevcut. Örneğin kısa yaşam süresine sahip bir Goroutine büyük bir stack allocation’a ihtiyaç duyarsa oldukça kısa süren bir işlem için büyük bir allocation yapmış olacağız. Ancak Split Stack implementasyonundaki sorunlara oranla bu ihtimal daha düşük.

Ekstra bilgiler

Goroutine’ler kendi stack boyutlarını 64-bit mimarilerde 1GB’a kadar 32-bit mimarilerde ise 250MB’a kadar genişletebiliyor.

Bir Goroutine yukarıda bahsettiğim limitlere ulaşana kadar daha fazla stack boyutuna ihtiyaç duyuyorsa runtime.morestack fonksiyonu çağırılıyor ve allocation yapılıyor. Eski stack sayfasının tamamı yenisine kopyalanıyor ve eskisi serbest bırakılıyor.

Eğer bu limit aşılırsa runtime.abort fonksiyonu çağırılarak Goroutine sonlandırılıyor. Sonrasında da aşağıdaki hata mesajı gösteriliyor

runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow

Goroutine’ler hakkında söyleyebileceklerim şimdilik bu kadar. Makalenin diğer kısmında ise Go scheduler’a odaklanacağız. Bir sonraki makalede görüşmek üzere. İyi kodlamalar.

İletişim adresleri :

--

--