Go Esintili Eşzamanlılık: POSIX İş Parçacıklarıyla Go'nun Concurrency Modeline Yaklaşmak Siber Güvenlik

Go Esintili Eşzamanlılık: POSIX İş Parçacıklarıyla Go'nun Concurrency Modeline Yaklaşmak

Go'nun hafif goroutinleri ve kanallarıyla ünlü eşzamanlılık modelini, POSIX iş parçacıkları kullanarak C'de ne kadar yaklaşık olarak yeniden üretebili

Go programlama dilinin en sevilen özelliklerinden biri, hiç şüphesiz eşzamanlılık modelidir. `go f()` yazarak bir fonksiyonu hafif bir goroutine olarak başlatmak, kanallar aracılığıyla veri alışverişi yapmak ve runtime scheduler'ın binlerce goroutine'i birkaç işletim sistemi iş parçacığında verimli bir şekilde yönetmesi, geliştiricilere adeta zahmetsiz bir deneyim sunar. Peki, tüm bu mekanizmanın bulunmadığı C dilinde, POSIX iş parçacıklarını (pthreads) kullanarak Go'nun eşzamanlılık modeline ne kadar yaklaşabiliriz? Bu soru, Solod (So) adlı, Go'nun sıkı bir alt kümesini C'ye dönüştüren bir derleyici projesi üzerinde çalışırken aklıma takıldı. So, runtime ve garbage collector olmadan çalışan bir dildir. Bu yazıda, seçtiğim pthreads tabanlı eşzamanlılık modelini, avantajlarını ve sınırlamalarını derinlemesine inceleyeceğiz.

So'nun eşzamanlılık yığınındaki her şey, iki temel POSIX ilkel öğesi üzerine inşa edilmiştir: mutex (karşılıklı dışlama) ve condition variable (koşul değişkeni). `sync.Mutex`, `pthread_mutex_t` etrafında ince bir sarmalayıcıdır. So kodunda `Lock()` metodu `pthread_mutex_lock`'u çağırır ve hata durumunda panic üretir. Bu dönüşüm neredeyse mekaniktir; So kodu, C'ye çevrildiğinde gürültülü olsa da aynı yapıyı korur. `sync.Cond` ise `pthread_cond_t`'yi sarmalayarak `Wait`, `Signal` ve `Broadcast` işlemlerini sunar. Bu iki tip, Once, thread havuzu ve kanallar gibi diğer eşzamanlılık araçlarının temelini oluşturur. Bu yaklaşımın performans üzerinde çeşitli etkileri vardır; zira her kilit işlemi bir sistem çağrısına dönüşebilir ve bu da maliyetlidir.

Her şey için kilit kullanmak gerekmez. So'nun `sync/atomic` paketi, Go'nunkini yansıtarak `Bool`, `Int32`, `Int64`, `Uint32`, `Uint64` ve genel `Pointer[T]` tiplerini `Load`, `Store`, `Swap` ve `CompareAndSwap` metotlarıyla sunar. Bu atomik işlemler pthreads gerektirmez; doğrudan C derleyicisinin `__atomic` yerleşik işlevlerine eşlenirler. Bu, Go'nun derleyicisinin kullandığı donanım talimatlarının aynısıdır. Performans karşılaştırmaları, tek bir iş parçacığında atomik işlemlerin Go ve So'da neredeyse aynı sürede tamamlandığını gösterir: Load ve Store 2 nanosaniye, CompareAndSwap ise 13 nanosaniye civarındadır. `sync.Once` da atomik işlemlerin etkin kullanımına güzel bir örnektir; hızlı yol (`fast path`) yalnızca bir atomik yükleme gerektirir ve fonksiyon bir kez çalıştırıldıktan sonra her `Do` çağrısı bir bayrağı kontrol ederek geri döner.

Teknik Analiz

Gerçek anlamda eşzamanlı kod çalıştırmak için iş parçacıklarına ihtiyaç vardır. `conc.Thread` tipi, `pthread_t` ve ilgili fonksiyonları sarmalar. `conc.Go` fonksiyonu, yeni bir işletim sistemi iş parçacığı oluşturur ve belirtilen fonksiyonu argümanıyla birlikte çalıştırır. Bu, Go'daki `go` anahtar kelimesine benzer, ancak ağır bir iş parçacığı oluşturur. Örnek kullanım: `th := conc.Go(work, &acc)` ve ardından `th.Wait()` ile iş parçacığının tamamlanmasını beklemek. Bu yaklaşım, goroutinlerin hafifliğinden yoksundur; her iş parçacığı kendi yığınına sahiptir ve oluşturma maliyeti yüksektir. Ancak yine de, birçok senaryoda işlevseldir.

Kanallar, Go'nun eşzamanlılık modelinin kalbidir. So'da kanal uygulaması, bir mutex ve bir veya daha fazla condition variable kullanır. `chan T` tipi, bir tampon, bir kilit ve gönderme/alma işlemlerini koordine eden koşul değişkenlerini içerir. Tamponlu kanallar, verileri geçici olarak depolar; tamponsuz kanallar ise doğrudan gönderici ve alıcı arasında senkronizasyon sağlar. `send` işlemi, kanal dolana kadar bekler; `recv` işlemi ise kanal boşalana kadar bekler. Bu, Go'daki kanal semantiğini birebir yansıtır. Ancak her kanal işlemi bir kilit ve koşul değişkeni içerdiğinden, yüksek frekanslı kullanımlarda performans düşüşü yaşanabilir. Performans testlerinde, tamponsuz bir kanal üzerinden 1 milyon mesaj gönderip almak Go'da yaklaşık 0.2 saniye sürerken, So'da bu süre 1.5 saniyeye çıkmaktadır. Tamponlu kanallarda ise fark daha azdır: 64 elemanlı tamponla Go 0.15 saniye, So 0.4 saniye.

Bu performans farkının temel nedeni, Go'nun goroutinlerinin işletim sistemi iş parçacıklarına göre çok daha hafif olmasıdır. Goroutinler, kullanıcı alanında yönetilir ve yığınları dinamik olarak büyüyüp küçülebilir. POSIX iş parçacıkları ise çekirdek tarafından yönetilir ve her biri sabit bir yığın boyutuna (genellikle 8 MB) sahiptir. Bu nedenle, binlerce iş parçacığı oluşturmak bellek ve zaman açısından pahalıdır. So'da bu sorunu aşmak için bir thread havuzu kullanılabilir. Havuz, önceden oluşturulmuş iş parçacıklarını yeniden kullanarak oluşturma maliyetini azaltır. Ayrıca, iş parçacığı sayısını sınırlayarak aşırı yüklenmeyi önler. Ancak yine de, her iş parçacığı bir çekirdek kaynağı tükettiği için, havuzun boyutu dikkatli seçilmelidir.

Tasarım açısından, So'nun eşzamanlılık modeli bazı ödünleşimler içerir. Öncelikle, POSIX iş parçacıklarıyla çalışmak, taşınabilirliği artırır; çünkü pthreads hemen her platformda mevcuttur. İkinci olarak, C'ye derlenen kod, Go'nun runtime'ına bağımlı olmadığı için daha öngörülebilir ve düşük gecikmeli sistemler için uygundur. Ancak, kanal gibi yüksek seviyeli soyutlamaların altında yatan kilit ve koşul değişkeni mekanizması, özellikle çok sayıda iş parçacığı rekabet ettiğinde performansı olumsuz etkileyebilir. Bu nedenle, So'da eşzamanlılık kullanırken, kritik bölümleri olabildiğince küçük tutmak ve mümkünse atomik işlemleri tercih etmek önemlidir.

Önemli Gelişmeler

Sonuç olarak, Go'nun eşzamanlılık modelini POSIX iş parçacıklarıyla C'de taklit etmek mümkündür, ancak bazı ödünleşimleri kabul etmek gerekir. So projesi, bu yaklaşımın işe yaradığını göstermektedir. Performans farkı, özellikle çok sayıda eşzamanlı işlem gerektiren senaryolarda belirginleşir. Ancak, thread havuzu ve dikkatli tasarım ile bu fark birçok uygulama için kabul edilebilir seviyeye indirilebilir. Eğer Go'nun zarif eşzamanlılık modelini C benzeri bir ortamda kullanmak istiyorsanız, So ve benzeri yaklaşımlar ilginizi çekebilir. Unutmayın: her soyutlamanın bir bedeli vardır; önemli olan, bu bedeli ödemeye değip değmeyeceğini bilmektir.

Kaynak: antonz.org

Paylaş: