~/Ali GÖREN

Olmaz Öyle Concurrency — 1 ve Golang

Ali Goren · · 6 dk okuma

Olmaz Öyle Concurrency — 1 ve Golang


Herkese selamlar. Bu yazıda Go dilinde concurrency ile ilgilenirken karşılaşabileceğiniz hataları dile getirmeye çalışacağım. Tabii bu hatalara karşı nasıl çözümler üretebiliriz onları da göreceğiz. Bu konuları başlık başlık incelemeye çalışacağız.

Data Race

Arkadaşlar, data race dediğimiz şey aynı anda en az 2 thread’in, aynı dataya eriştiği ve en azından birinin bu dataya veri yazmaya çalıştığı anda ortaya çıkabilir. Öncelikle sorun nasıl oluşuyor ona bakalım.

Data race oldukça yaygın bir bug türüdür. Ve debug işlemleri de bir tık zor olabilir.

Data Race Oluşturalım ve Düzeltelim

Bu nasıl yazı ya bize buglı kod öğretiyor…

Yukarıdaki koda baktığımızda 2 farklı goroutine görmekteyiz. Bunları ikiz kardeşler gibi düşünün. A1 ve A2 dediğimiz bu goroutine’ler data race oluşturmakta. Ve açıkçası, yukarıda yer alan örnekte goroutine’in hangi sırada çalışacağını bilmiyoruz. 14. satırda ise data race işlemi ortaya çıkıyor. Aynı anda 2 goroutine buraya erişiyor ve hangisinin veriyi yazdığı açıkcası bilinmiyor.

Bu kısımda ufak bir düzeltme aldım. Utku Özdemir’e çok teşekkür ederim bunun için.

Data race işleminin var olup olmadığını şöyle algılayabiliriz. Go bu konuda yardımcı bir tool’a sahip.

go run -race main.go

Ve bu komut sonucunda eğer data race var ise şuna benzer bir çıktı elde edersiniz

Buradan anladığımız bir misordering işlemi yani sıralamanın garanti olmadığı bir durum ortaya çıkıyor. Şimdi bu kodu nasıl thread safe yaparız ona bakalım.

Genel olarak konuşacak olursak, data race’i engellemek istiyorsak threadler arasında paylaşılan mutable datalara senkronize bir erişim gerçekleştirmeliyiz. Go dili buna channel ya da lock mekanizmalarını getirmiş. Ayrıca sync paketi de çok güzel farklı çözümlere sahip.

Go dilinde concurrent data sharing işlemi için önerilen yöntem channelların kullanımıdır. Hatta Go Proverbs’de buna değinilmiştir.

Don’t communicate by sharing memory, share memory by communicating.

Bellek paylaşımı ile iletişim kurmak yerine, iletişim kurarak bellek paylaşın diyor kısacası.

Peki yukarıdaki buglı kod nasıl fixlenir?

Evet böyle fixleniyormuş.

  1. satırda yer alan local a değişkeni sadece ilgili goroutine içerisinde görülebilirdir.

  2. satırda a datası, bir başka goroutine’e devredilmek üzere yola çıkıyor. Hani channellar hakkında pipe gibiler diyorduk ya. Şimdi veri bu pipe içerisinde ilerliyor diyelim.

  3. satıra gelindiği anda, bu pipe içerisindeki data yeni alıcısına güvenli bir şekilde ulaşıyor.

Burada channel senkronizasyonu sağlama görevini üstlenir böylece data bir goroutine’den bir başkasına güvenle iletilir. Burada 2 işlem gerçekleşiyor. Sıralamaya aldırmadan işleyişi açıklayayım.

Bir goroutine, diğerinin verileri almasını beklerken, diğeri de bir başkasının verileri göndermesini bekler.

go run -race main.go

İsterseniz yukarıdaki komutu tekrar çalıştırıp data race oluşmadığına dair kanıtı yani yokluğu görebilirsiniz.

Bu işlemi WaitGroup kullanarak şöyle de yapabilirdik

Deadlock


Arkadaşlar, deadlock dediğimiz şey genel olarak konuştuğumuzda, 2 farklı process birbirini beklediği anda ve herhangi bir ilerleme sağlayamadığında olur. Bu goroutine’ler açısından da böyledir. Dört arabanın bir kavşakta birbirine yol vermeyi beklemesini bir deadlock örneği olarak verebiliriz.

Bu terimi birçok yerde duymuş olabilirsiniz. Örneğin DBA’ler sıklıkla bahseder bu konudan. Önlemek için;

  • Mutex kullanılabilir (Mutual Exclusion)
  • Kaynaklar aynı anda değil, sıralı olarak kullanılmalı. Yani X alınmadan Y’yi kullanmamak gerekiyor
  • Wait for döngüsü kullanılabilir böylelikle eğer kaynak o anda erişilebilir değilse bile belli aralıklarla kaynağın erişilebilirliği kontrol edilmelidir
  • Pre-emption olayının engellenmesi. Yani 6 ay öncesinden otel yeri ayarlama işlemini burada yapmamak gerekiyor. Zamanı geldiğinde kaynak tahsis edilmelidir.

Go tarafında da deadlock oluşumu benzerdir.

Bir goroutine, boş bir channel’dan değer almaya çalışırsa ve başka bir goroutine yok ise deadlock oluşabilir. Go dilinde basit bir deadlock oluşturma işlemini şöyle yapabiliriz;

Sender’ın olmadığı bu kodun çıktısı şöyle olacaktır. Burada yaşanan şey, unbuffered bir channel oluşturmak ve hiçbir dinleyen olmadan yazmaya çalışmak ve bu aşamada da tek bir goroutine olduğu için deadlock oluşur.

Bu kodu şöyle düzeltebiliriz

Ayrıca receiver’ın olmadığı bir go kodunda yine şu şekilde deadlock oluşturabiliriz. (Çözümü yukarısıyla aynı olduğu için vermiyorum tekrarını)

Ve çıktısı da böyle olacaktır

Tabii her iki örnekte yer alan outputlar farklı değil gibi görünse de birisi chan send bir diğeri de chan receive işleminde bu deadlock oluştu diyor.

Buraya kadar anladık ki unbuffered bir channelda dinleyen yokken veri yazma ya da alma işlemi yapıyoruz ve deadlock oluşuyor. Peki başka şekillerde oluşabilir mi? Evet oluşabilir. Örneğin yukarıda bahsettiğimiz WaitGroup’lar deadlock yaratabilirler.

Yukarıdaki kodun çıktısı şudur arkadaşlar

Buradaki problem şu oluyor arkadaşlar. Diyelim ki for döngüsünü 10 kere döndürüyoruz. Bunu mesela, concurrent olarak siteleri parse eden bir program olarak düşünün.

10 site var ama siz bunu gidip 11 tane goroutine bekleyecek bir WaitGroup olarak tanımladınız. Haydaa, 1 tane goroutine fazladan bekliyor da bekliyor. Bu nedenle deadlock oluşuyor. Bunu çözmenin yolu ise, loop counter kadar goroutine olacağını söylemektir.

Bu noktada WaitGrouplar tarafından yaratılan deadlock sorununu da gördük. Tabii keşke bu kadar basit olarak ele alınacak deadlock problemleri ile karşılaşsak güzel olur ama gerçek dünya problemleri pek öyle olmuyorlar…

Deadlock oluşturan bir diğer hata ise, lockinglerin unutulması. Yani dediniz ki ben ne kadar istersem o kadar locklarım. Ee güzel lockla bakalım… Sonra bu lockinglerden birisini orada unutursanız bir bug sizi bekliyor. Ve bu runtime’da oluyor. Acı noktası da burası.

Yukarıdaki kod anında bir deadlock hatasını size gösterecektir. Çıktıları artık vermesek daha iyi. Örneğe göre 2 lock, 2 unlock var ise problem yaşamamalıyız. Ancak mantık sizi a noktasından b noktasına götürür, deadlock ise her yere diyorum.

Eğer art arda aynı lock 2 defa çağırılıyor ise, bu bir deadlocktır. Burada çözüm aranıyor ise, lock işleminin tamamlandığı noktaya bir tane unlock çağrısı yapılmalıdır. Kodu biraz daha uzatalım ve şöyle yapalım;

Gördüğünüz gibi aynı lock, unlock edilmeden 2 defa çağırılmadı ve deadlock problemimiz çözüldü.

Bir diğer deadlock ise select ifadelerinde ortaya çıkabiliyor.

Yukarıdaki kod bir deadlock yaratacaktır. Sayi, herhangi bir değer almadığı için deadlock oluşuyor. select ifadelerinde deadlock’i engellemek için default kullanabiliriz.

Normalde olsaydı bu kod şöyle çalışırdı

Hepsi bu kadar arkadaşlar. Bu yazı birazcık uzadığı için diğer konu başlıklarını farklı bir yazı altına toplamayı düşünüyorum.

Gelecek konu başlıkları ise Livelock, Starvation gibi konular üzerine olacak.

Bu yazıyla ilgili sorularınızı bana aşağıdaki kaynaklardan ulaşarak sorabilirsiniz;

Kaynaklar

Wingie / Enuygun’un büyüyen ekibinin bir parçası olmak isterseniz buradan açık pozisyonlarımıza göz atabilirsiniz. Tech ekibimize başvurmak için ise CV’nizi kariyer@enuygun.com’a iletebilirsiniz.