iOS ve Concurrency (Eşzamanlılık)-Thread Safety

Ayşe Nur Bakırcı
Delivery Hero Tech Hub
5 min readMar 8, 2022

Concurrency ile çalışmaya başladığımızda bilmemiz gereken en önemli konulardan birisi thread safety konusudur. Concurrency ile birlikte birden çok thread ve bu thread’ler üzerinde ilerleyen birden çok görevle uğraşmaya başlarız. Bu işler doğru takip edilmediğinde doğal olarak bize sorun çıkartır. Concurrency ile daha hızlı ve kesintisiz bir uygulama oluşturmaya çalışırken, kendimizi çıkılmayacak bir durum içerisinde bulabiliriz. Kaçınmamız gereken bu durumlardan bazıları aşağıda anlatılmıştır.

Race Condition

Elimizdeki bir değişkene, birden fazla thread’in aynı anda erişimi olduğunda karşımıza çıkabilecek bir durumdur. Yukarıdaki örneği incelersek; başlangıç değeri 1 olan bir değişkenin değeri, farklı thread’ler üzerinden iki kez arttırılmaya çalışılmıştır. İlk thread henüz yeni değeri değişkene yazmadan ikinci thread değişkenin değerini 1 olarak okuduğu için bu iki işlemin sonucunda yeni değerimiz 3 olması gerekirken 2 olarak kalmıştır.

Bir değişkenin değerini birden fazla thread kontrol ediyorsa, karışıklık olmaması için gelen işlemleri sıraya alabilir ve race condition oluşmasını engelleyebiliriz. İşlemleri sıraya almamızın en kolay yöntemi ise seri kuyruk kullanarak bu değişkene yapılan işlemleri sırayla yaptırmak olacaktır.

Deadlock

Sync, wait, semafor gibi görevleri bekleten yapıları kullanıyorsak deadlock oluşmasına sebep olabiliriz. Bu durumda iki farklı görev birbirini engeller veya birbirini bekler. Sonuç olarak thread’ler sonsuza kadar bekleme sürecine girerler ve thread’de bulunan diğer görevler gerçekleşemez. Bu durumu daha iyi anlamak için aşağıdaki örneği inceleyebiliriz.

Bu örnekte ilk görev tamamlandıktan sonra ikinci görevin başlatılması gerektiği zaman deadlock oluşur. Bunun nedeni seri kuyruklarda bir görevin önceki görev bittikten sonra başlatılabilmesidir. Örneğe baktığımızda, 2. görevin başlatılabilmesi için, 1. görev tamamlanmalıdır. Ancak 2. görev, 1. görevin içinde olduğundan 1. görevin tamamlanmış olabilmesi için 2. görevin de tamamlanmış olması gerekir. Bu çakışma deadlock oluşmasına ve uygulamanın çökmesine sebep olur.

Priority Inversion

Bir kuyruğa, kuyruğun QoS(Quality of Service) değerinden daha yüksek seviyeli bir görev gönderdiğimizde priority inversion oluşur ve kuyruğun QoS değeri görevin değerine yükseltilir. Bu yeni değer kuyrukta bulunan tüm görevlerde geçerlidir. Kuyruğun QoS değeri yükseldiğinde, düşük öncelikli olarak planladığımız görevleri daha yüksek öncelikle yaptığımız için düşük önceliğe sahip görevlerin önemli görevlerimizin önüne geçmesine sebep olabiliriz. Bu durum oluştuğunda görevler doğru sırayla yapılmayabilir veya uzun süren background görevleri yüksek öncelikle yapılarak kullanıcıyı etkileyebilir. Bu yüzden görevlerin önceliklerine dikkat ederek, doğru kuyruklara göndermeliyiz.

Thread safety için asenkron görevlerle birlikte kullanabileceğimiz bazı sınırlamalar da mevcuttur. Bu sınırlamalar görevleri doğru şekilde ilerletebilmemiz için bize yardımcı olur.

Thread Barrier

Asenkron bir şekilde ilerlerken, görevlerin devam etmesi için bir görevin tamamlanması gerekebilir. Örneğin yukarıdaki örnekte yapılması gereken 6 tane işimiz var. Bu kuyrukta ilk 3 görev bittikten sonra 4. görevin yapılması ve 4. görevden sonra diğer görevlerin devam etmesi gerekiyor. Bu gibi durumlarda göreve bir flag ekleyerek görevlerin istediğimiz şekilde ilerlemesini sağlayabiliriz. Bu durumu bir örnekte inceleyecek olursak;

Yukarıdaki örneği incelediğinizde 🍍 görevine bir bariyer eklediğimizi farketmişsinizdir. Sonuçta da gördüğünüz gibi 🍍 görevinin öncesindeki tüm görevler tamamlandıktan sonra 🍍 görevi başlatılır ve tamamlanır. 🍍 görevinden sonra kuyruk tekrar asenkron şekilde çalışmaya devam eder.

Semaphore

Semaforları, elimizde aynı anda yapabileceğimiz çok fazla iş olduğunda bu işleri parçalar halinde yapmak için kullanabiliriz. Örneğin, yapmamız gereken 15 gibi fazla sayıda görev olduğunu düşünelim, bu görevlerin aynı anda yapılması yerine aşama aşama, 2 veya 3 gibi küçük sayılarda görev grupları halinde ilerlemesini istediğimiz durumlarda semaphore’ları kullanabiliriz.

Thread Barrier için yaptığımız örneği biraz değiştirerek semaphore’lara uygun hale getirip inceleyelim. 🙂

Semaphore’u oluştururken verdiğimiz başlangıç değeri aynı anda kaç adet görevi devam ettirmek istediğimizi temsil eder. Yukarıdaki örnekte verdiğimiz 1 başlangıç değeri semaphore sayacına vereceğimiz başlangıç değeridir. Peki semaphore sayacı ne? Bunları nasıl kontrol ediyoruz? Hepsinin cevabı wait ve signal methodlarında. 🙂

Semaphore’ları ilerletirken kullanmamız gereken iki farklı method mevcuttur. Bunlar;

  • signal(): Signal, görevi tamamlandığında çağrılmalıdır. Signal methodu bizim semaphore sayacını 1 arttırmamızı sağlar. Bu da semaphore’da bir görev için yer açıldığı anlamına gelir.
  • wait(): Wait, bir göreve başlamadan önce çağrılmalıdır. Semaphore’a yeni bir göreve başlayacağımızı wait ile belirtiriz. Wait methodu da semaphore sayacını 1 azaltmamızı sağlar.

Yukarıdaki örnekte semaphore sayacının başlangıç değeri 1 verilmiştir. Bu aynı anda iki göreve izin verir. İlk görevi başlattığımızda wait methodu çağrılır, bu sayede sayaç 1 azalır ve 0 olur. Bir görevi daha başlattığımızda ise sayaç 0'dan daha küçük bir değer alır ve kuyruk bu görevlerden birisi tamamlanana kadar donar. Görevlerden birisi bitip signal methodu çağrıldığında ise semaphore sayacı 1 arttırılır ve semaphore’da bir görev için boş yer açılır. Bu boşluk bir sonraki görevle doldurulur ve aynı anda maksimum iki görev ilerletilecek şekilde görevler tamamlanmaya devam eder.

Bir önceki yazımda concurrency konusundan bahsetmiştim. Eğer incelemek isterseniz buradan ulaşabilirsiniz. Okuduğunuz için teşekkür ederim. Yorum ve önerilerinizi benimle paylaşabilirsiniz. 🙂

--

--