Race Condition nedir ve nasıl önlenir?

Bu flood’da çok thread’li uygulamalarda karşılaşılan Race Condition probleminin ne olduğunu ve hangi yöntemlerle önlenebileceğini anlatacağım.

Race Condition birden çok thread'in paylaşılan bir hafıza alanına aynı anda ulaşıp o alanı değiştirmesiyle meydana gelir. Race Condition'lar genellikle uygulamanın doğru çalışmasını bozdukları için bug olarak adlandırılırlar.

En basit ve ağır şekli ile Race Condition'ın nasıl oluştuğunu anlamak için aşağıdaki iki thread'in bir sayacı 5M kere artırdığı ancak sayacın 2 * 5M = 10M yerine ~7.2M olduğu durumu ele alalım.

Kodlardan da görüldüğü üzere iki thread eş zamanlı olarak başlatılarak sürekli olarak counter adlı değişkeni artırmaya çalışmışlardır. Thread'lerin counter'u artırmaktan başka bir iş yapmaması sebebiyle Race Condition en ağır şekliyle görülmektedir.

Şimdi toplam sayının 10M yerine neden ~7.2M olduğuna, yani Race Condition'ın nasıl oluştuğuna bir seviye aşağı inerek daha yakından bakalım.

Yukarıdaki detaylandırılan işlemlerden görüldüğü üzere counter = counter + 1 işlemini gerçekleştirmek için işlemci register'larını da içeren bir dizi işlemler yapılmaktadır. Bu işlemler bir bütün olarak gerçekleştirilemediğinden Race Condition'lar ortaya çıkmaktadır.

Artık Race Condition'ların nasıl oluştuğunu anladığımıza göre Race Condition'lardan nasıl kaçınabileceğimize bakabiliriz. Önceki flood'ların birinde https://medium.com/@gokhansengun/ba552a17c03 hatırlayacağınız üzere Critical Section'lardan bahsetmiştik.

Paylaşılan hafıza alanına erişim yapılan kodlar Critical Section içine alınırsa aynı kod bloğu aynı anda sadece bir thread tarafından çalıştırılır ve dolayısıyla Race Condition oluşması engellenmiş olur.

Aşağıda bir önceki örnekte Race Condition oluşturan sayaç artırma örneğinde sayaç artıran kod bloğu Critical Section içerisine alınarak Race Condition oluşumunun engellendiği gösterilmiştir.

Critical Section'ların Race Condition'ı engelleme maliyeti tahmin edilebileceği gibi performanstır. Aşağıdaki şekilden de görülebileceği üzere Critical Section'ın dahil olması ile işlem süresi yaklaşık üç katına çıkmıştır.

Performansın kritik olduğu sistemlerde Critical Section kullanımını minimize edecek önlemlere başvurulmalıdır. Örneğin bu tip sistemlerde uygulama yapısını değiştirerek Critical Section'lar azaltılabilir ve Lock-free veri yapılarını tercih etmek gerekebilir.

Performansın çok kritik olmadığı veya ortak kaynaklara erişimin fazla olmadığı sistemlerde ise kod kompleksliğini fazla artırmamak adına Critical Section kullanarak Race Condition'lardan korunmak ve kodu sade bırakmak iyi bir fikirdir.

Yukarıda gösterildiği üzere Race Condition'ları önlemek üzere sürekli olarak Critical Section'lara girilmesi performansı olumsuz olarak etkilemektedir. Double-checked Locking yöntemi ile bazı kullanım senaryoları için Critical Section maliyeti azaltılabilir.

Örnek olarak birçok thread tarafından ortak kullanılacak Singleton bir objenin oluşturulması için bir kod bloğu yazdığımızı düşünelim. Burada biri Critical Section içinde bulunmak üzere iç içe iki if kullanılarak objeyi sadece ilk thread'in yaratmasını sağlayabiliriz.

Aşağıdaki ekran görüntüsünde Double-checked Locking bir Python kodu ile örneklenmiştir. Görüleceği üzere Critical Section'a ilk ulaşan thread bir kereliğine objeyi oluşturmuş diğerleri ise oluşturulan bu objeyi kullanımış yeni objeler oluşturmamıştır.

Popüler işlemciler bir değişkenin Atomic olarak yani başka thread tarafından bölünemez bir şekilde değiştirilmesine olanak tanırlar. Ortak kullanılan değişkenlerin kullanılan dilin sunduğu Atomic kütüphaneler vasıtasıyla erişilmesiyle de Race Condition'dan kaçınılabilir.

Bazı Race Condition'lar çok uzun sürede ancak tekrarlanabildiği için haklarında bilgi toplanması ve çözülmesi zor problemler olarak karşımıza çıkarlar. Bu nedenle Race Condition'dan kaçınmanın en iyi yolu bilinç düzeyini artırmak ve kod gözden geçirmelerini sıkılaştırmaktır.