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

Ayşe Nur Bakırcı
Delivery Hero Tech Hub
8 min readFeb 2, 2022

Herkese merhaba, bu yazımdaki konumuz Apple dökümanlarında Concurrency olarak gördüğümüz eşzamanlılık. Bu konunun benim gibi mobil uygulama geliştirmeye yeni başlayan kişilerin henüz tanışmadığı ama öğrenince de neden öğrenmemişim diyeceği bir konu olduğunu düşünüyorum. En azından benim için öyle oldu.🙂

Yazıya başlarken temel olarak eşzamanlılığın ne olduğuna bakmak yazıyı anlamamızı kolaylaştıracaktır. Bir uygulama düşünelim, bu uygulamaya işleri aynı anda yapabilme yeteneğini kazandırdığımızda bu uygulamanın işlerini eşzamanlı olarak ilerletmiş oluruz. Yani kısaca eşzamanlılık aynı anda birden fazla işi yapabilme yeteneğidir. Uygulamamız işleri aynı anda yürüttüğünde daha hızlı yanıt verebilir ve kullanıcı için kesintisiz bir uygulama deneyimi oluşturmamızı sağlar.

Peki eşzamanlılığı nasıl sağlıyoruz? Belki thread’leri duymuşsunuzdur. Thread’leri işlerimizi üzerinde ilerlettiğimiz iplikçikler gibi düşünebiliriz. Kullandığımız cihazda çekirdek sayısına bağlı olarak aynı anda kullanabileceğimiz birden fazla thread mevcuttur. Bu threadleri ve üzerinde yürüttüğümüz işleri kontrol ederek eşzamanlılığı sağlayabiliriz. Burada thread’leri yönetmek derken korkmaya hiç gerek yok, uygulama içerisinde direkt thread’lerle uğraşmayacağız.🙂 Apple belli API’lar ile bunu bizim için oldukça kolay hale getirmiş zaten.

Uygulamanızın çalışmasını birden çok iş parçacığına bölmenin birçok avantajı vardır. Bunlar;

  • Daha hızlı yürütme: Görevleri farklı iş parçacıklarında yürüterek, görevlerin birbirinden bağımsız ve daha hızlı şekilde tamamlanmasını sağlayabiliriz.
  • Duyarlılık: Arka planda gerçekleşen işleri main thread dışında bir thread’de yaparak kullanıcının yaşayacağı donma gibi sorunları en aza indirebiliriz.
  • Optimize edilmiş kaynak tüketimi: İş parçacıkları, işletim sistemi tarafından optimize edilmiştir. Bu yüzden uygulamanın kaynak tüketimi konusunda bize yardımcı olabilir.

Sıra geldi bu eşzamanlılığı nasıl gerçekleştirebileceğimiz kısmına.🙂Eşzamanlılık için kullanabileceğimiz iki yapı mevcuttur. Bunların en çok kullanılanı GrandCentralDispatch yani GCD, diğeri ise Operation’lardır. Aslında yapılan işlemlerin temeline baktığımızda Operation da GCD’nin soyutlanmış halidir. Biz Operation ile çalıştığımızda arka planda dolaylı olarak GCD ile çalışmış oluruz. Ancak oluşturulması ve üzerinde yaptığımız işlemler ayrıntılı olduğu için ayrı bir başlık olarak ele alınmaktadır.

GrandCentralDispatch (GCD)

Bir uygulamanın üzerinde çalıştığı çok çekirdekli donanımdan yararlanarak eşzamanlı çalışmak istediğimizde aklımıza ilk olarak GCD gelmelidir. GCD içerisinde belirli özelliklere sahip FIFO (First In First Out) kuyruklarını içerir. Bu kuyruklara görevleri eklediğimizde, sistem bizim için thread’lerin yönetilmesini sağlar.

DispatchQueue

GCD ile iletişim kurarak farklı threadler ile çalışabilmek için öncelikle DispatchQueue oluşturmamız gerekir. Bir kuyruk oluşturup görevleri bu kuyruğa atarak yürütmeye çalıştığımızda, sistem gerekli thread’leri oluşturarak görevlerin farklı thread’ler üzerinde yürütülmesini sağlayacaktır. Proje içerisinde projenin kendine ait farklı özelliklerde dispatch queue’ları bulunur. Kullanılabilir kuyruklar yalnızca önceden tanımlanmış kuyruklarla sınırlı değildir. Eğer istersek kendimiz de farklı kuyruklar oluşturabiliriz. Ancak dikkat etmeliyiz, donanımın aynı anda çalıştırabileceği thread sayısı sınırlıdır. Bu yüzden çok fazla thread oluşturmamız uygulamayı iyi yönde etkilemeyecektir.

DispatchQueue Oluşturmak

DispatchQueue ile kuyruk oluştururken öncelikle belirlememiz gereken özellik bu kuyruğun seri (serial queue) mi yoksa eşzamanlı (concurrent queue) mı olması gerektiğidir. Seri kuyruklar tek bir thread’e sahip olabiliyorken, eşzamanlı kuyruklar farklı sayılarda (donanımın izin verdiği kadar) thread’lere sahip olabilir. Bunları tabii ki ihtiyaçlarımıza göre belirliyoruz.🙃

Seri Kuyruk (Serial Queue) Oluşturmak

Yukarıda da bahsettiğim gibi seri kuyruk olarak adlandırdığımız kuyruklar yalnızca tek bir thread’e sahip olan kuyruklardır. Tek bir thread’e sahip olduğu için de aynı anda yalnızca bir görevin başlatılmasına izin verebilir.

Seri kuyruklara verebileceğimiz en önemli örnek Main Queue’dur. Main queue, uygulama başlatıldığında otomatik olarak oluşturulan ve uygulamanın UI işlemleri gibi büyük öncelik taşıyan görevleri ilerleten bir seri kuyruktur.

Seri bir kuyruk üzerinde ilerletmemiz ve ana kuyruğu etkilememesi gereken işlemlerimiz olduğunda DispatchQueue ile yeni bir seri kuyruk oluşturabiliriz. Seri kuyruk oluştururken dikkat etmemiz gereken tek nokta kuyruğa verilen label değerinin benzersiz olmasıdır.

Eşzamanlı Kuyruk (Concurrent Queue) Oluşturmak

Birden fazla thread’e sahip olduğu için aynı anda birçok görevi ilerletebilen kuyruklar eşzamanlı kuyruklardır. Uygulama içerisinde var olan eşzamanlı kuyrukları kullanmak istediğimizde global kuyrukları kullanabiliriz.

Eğer yapacağımız görevler için özel bir kuyruk oluşturmak istersek DispatchQueue ile yeni bir kuyruk oluşturabiliriz. Eşzamanlı kuyruk oluşturmak istediğimizde seri kuyruklardan farklı olarak attributes değerini .concurrent olarak belirtmemiz gereklidir. Eğer attibutes değerini vermezsek, bu değerin varsayılan olarak bize seri kuyruk oluşturur.

Oluşturduğumuz DispatchQueue’ları Kullanmak

Thread’leri, bu thread’lerin kuyruklarla olan ilişkisini, seri ve eşzamanlı kuyrukların farklarını ve bunların nasıl oluşturulduğunu inceledik. Sıra oluşturduğumuz bu kuyrukları nasıl kullanacağımızı öğrenmeye geldi. Ancak bu kuyrukları nasıl kullanacağımıza bakmadan önce anlamamız gereken iki önemli konu var. Bunlardan ilki QoS değerleri diğeri ise Senkron ve Asenkron kavramlarıdır.

— Quality Of Services (QoS)

Kuyruklar ile çalışırken bu kuyrukların veya görevlerin hizmet kalitesini tanımlayabiliriz. Bu yapı sayesinde görevlere öncelikler vererek kaynakların uygun görevlere ayrılmasını, uygulamanın daha duyarlı olmasını ve pil tasarrufu yapabilmeyi sağlayabiliriz. Kullanabileceğimiz altı farklı QoS değeri vardır.

  • .userInteractive: Kullanıcının doğrudan etkileşim halinde olduğu UI ve animasyonlar gibi işlemlerde kullanılması önerilir. Bu kuyrukta ilerletilen görevler anında tamamlanabilecek görevler olmalıdır.
  • .userInitiated: Kullanıcı ile bağlantılı olduğu için hemen gerçekleşmesi gereken ancak eşzamanlı da ilerletilebilinen görevlerde kullanılması önerilir. Örneğin local veritabanından veri okumamız gerektiğinde kullanabiliriz. Bu kuyruktaki görevler en fazla birkaç saniye sürecek görevler olmalıdır.
  • .default: Kullanıldığında bir QoS değeri tanımlanmamış olarak varsayılır ve işlem önceliğine sistem karar verir. Sistem her şeyi planlayabiliyorken bu seçimi sisteme bırakmak ne kadar doğru görünse de default kullanımı her zaman doğru bir yaklaşım olmayacaktır. Çünkü görevleri yürütürken belirlediğimiz öncelik görevlerin doğru çalışabilmesi için önemlidir. Beklediğimiz önceliği sistem vermezse bu görevlerin yürütülmesinde sorun oluşturabilir.
  • .utility: Genellikle uzun sürebilecek hesaplamalar, sürekli veri beslemeleri gibi devam eden işlemlerde kullanılır. Bu önceliğe sahip kuyruklar üzerinde görevler ilerlerken sistem performans ve enerji verimliliğini dengede tutmaya çalışır. Bu kuyruktaki görevlerin süresi birkaç dakikayı geçmemelidir.
  • .background: Kullanıcıyı doğrudan etkilemeyen, arka planda gerçekleşen, veritabanı bakımı, uzak sunucu senkronizasyonu gibi işlerde kullanılması önerilir. Sistem bu kuyruktaki görevler ilerlerken enerji verimliliğine öncelik verir. Bu kuyruğa eklenen görevler dakikalarca sürebilecek uzun görevlerdir.
  • .unspecified: Herhangi bir QoS bilgisinin olmadığını temsil eder. QoS bilgisinin olması gerektiği yerlerde kullanılması tavsiye edilmez.

NOT: Kuyrukları QoS değerini belirterek oluşturabiliriz. Ancak kuyrukların QoS değeri her zaman belirttiğimiz seviyede kalmayacaktır. Eğer bir kuyruğa kuyruğun QoS seviyesinden daha yüksek seviyeli bir görev gönderirsek, görevin yürütüldüğü kuyruğun QoS değeri sistem tarafından görevin QoS değerine yükseltilir. Bu yalnızca belirtilen görev için geçerli değildir. Sonrasında gelen görevler de kuyruğun güncel QoS değerine göre yürütülürler.

— Senkron (Synchronous) ve Asenkron (Asynchronous) Görevler

Oluşturduğumuz kuyruklara görevler ekleyip bu görevlerin yürütülmesini sağlarken, yürüttüğümüz görevlerin senkron mu yoksa asenkron mu ilerleyeceğini de belirtmemiz gerekir.

  • Senkron Görevler: Senkron çalışan görevler birbirini bekleyen görevlerdir diyebiliriz. Bir görevi bir kuyruğa senkron olarak gönderdiğimizde, kuyruktaki diğer görev başlatılmadan önce devam eden senkron görevin tamamlanması beklenir.
  • Asenkron Görevler: Asenkron çalışan görevler senkron görevler gibi birbirini beklemez. Bu görevler kuyrukta başlatıldıktan sonra farklı thread’lerde çalışabilir. Bir diğer görevin çağrılabilmesi için, başlatılan görevin bitmesine gerek yoktur.

Sürekli karşılaştığımız soru: Asynchronous (asenkronluk) ve concurrency (eşzamanlılık) aynı şey midir?

İlk bakışta ikiside birden fazla işlemin aynı anda yapılmasını sağlıyormuş gibi görünse de kuyrukları farklı noktalarda etkiler. Senkron veya asenkron olmak, bir görevi yürüttüğümüzde kuyruğun bir sonraki görevi yürütmeden önce devam eden görevi bekleyip beklememesi gerektiğini belirtirken, seri veya eşzamanlı olmak kuyruğun sahip olabileceği thread sayısının bir veya birden fazla olacağını belirtir.

İşte şimdi sıra geldi oluşturduğumuz bu seri kuyrukları kullanmaya. :)

— Seri Kuyrukları Kullanmak

Bu örneği incelediğinizde, dikkatinizi çeken kısım seri kuyruk kullanmamıza rağmen asenkron görevlerin farklı thread’lerde olduğu olabilir. Burada asenkron görevin mantığı gereğince görev başlatıldıktan sonra farklı thread’e devredilir. Aynı kuyruk üzerinde ilerleyen asenkron görevler farklı thread’lerde olsa da, kuyruk üzerinde bulunan bir iş bitmeden diğer iş başlamayacaktır.

Aynı zamanda kuyruğa senkron görev gönderdiğimizde yalnızca mevcut görev ilerletilirken, asenkron görev gönderdiğimizde kuyruktaki görevle birlikte kuyruğa dahil olmayan görevin de yürütüldüğünü farketmişsinizdir. Bu da seri kuyruklardaki senkron ve asenkron ilerleyişin farkını bize gösterir.

— Eşzamanlı Kuyrukları Kullanmak

Benim düşünceme göre, eşzamanlı kuyruklardaki senkron ve asenkron ilerleyişi seri kuyruklara göre daha anlaşılır. Eğer görevlerin beraber ilerlemesi bizim için bir sorun oluşturmayacaksa eşzamanlı kuyruklara asenkron görevler göndererek görevlerin daha hızlı ilerlemesini sağlayabiliriz. Ancak bu eşzamanlı kuyrukta bitmesini beklememiz gereken bir görev olursa, bu görevi senkron şekilde kuyruğa göndererek görev bittikten sonra diğer işlemlerin devam etmesini sağlayabiliriz.

DispatchWorkItem

Yukarıdaki örnekleri incelediğinizde farketmişsinizdir. Kuyruklara görevleri sync veya async methodlarında bulunan closure ile gönderiyoruz. Ancak bunun tek yolu bu closure’ın kullanımı değildir. DispatchWorkItem kullanarak bu kuyruklara görevler gönderebiliriz.

DispatchWorkItem’lar, görevleri oluşturmak dışında görevler üzerinde daha fazla kontrole sahip olmamızı da sağlar. Örneğin bir görevi iptal etmemiz veya bir diğer görev tamamlanana kadar bekletmemiz gerekebilir. Bu gibi durumlarda dispatchWorkItem’lar ile kullanabileceğimiz methodlar mevcuttur.

  • Notify: Bir dispatchWorkItem ile ilerlerken, bu görevden hemen sonra başlaması gereken bir görevimiz olduğunda bunu notify methodu ile belirtebiliriz.
  • Cancel: Bir dispatchWorkItem’ın yürütülmesini iptal etmemiz gerektiğinde cancel metodunu kullanabiliriz. Eğer görev henüz başlamamışsa görev yürütülmez, devam eden bir görevi iptal edersek görevin isCancelled özelliği true olur. Devam eden bir görevi iptal ettiğimizde görev durdurulmaz, tamamlanana kadar yürütülür.
  • Wait: Bir görevi bekletmek için kullanabiliriz ancak görevi bekletmek senkron ilerleyen bir işlem olduğundan görevin bulunduğu kuyruğu bloke edecektir. Yani main queue üzerinde kullanılmaması gerekir.

DispatchGroup

Görevlerlerle beraber bu görevleri ekleyebileceğimiz DispatchGroup’lar da mevcuttur. Birbirleriyle bağlantılı birden fazla görevimiz varsa bunlar DispatchGroup’lar aracılığıla yürüterek görevler üzerinde daha fazla kontrolümüz olmasını sağlayabiliriz.

Yukarıdaki örneği incelediğimizde iki farklı görevi bir gruba dahil ederek ilerlettik ve görevler üzerinde bazı kontrolleri denedik.

  • Wait: DispatchWorkItem’da da olduğu gibi grubun beklemesini sağlar. Yukarıdaki örneği inceleyecek olursak, wait ile grup 4 saniye bekletilerek bu süre içerisinde işin bitip bitmediği kontrol edilir. Süre geçtikten sonra iş durdurulmaz, yalnızca belirtilen sürede işin bitip bitmediğini kontrol etmiş oluruz. Burada dikkat etmemiz gereken nokta wait methodunun senkron ilerleyen bir method olduğunu ve bulunduğu kuyruğu bloke ettiğidir.
  • Notify: DispatchWorkItem’da kullandığımız notify ile aynı işleve sahiptir. DispatchGroup, içerisindeki görevlerin tamamlanıp tamalanmadığını takip edebilecek özelliklere sahiptir. En son tüm işler bittiğinde görevlerin bittiğini bildirmek veya görevlerden sonra yapılması gereken bir işi yapmak için notify kullanılabilir. Notify methodu Wait’in aksine asenkron olarak çalışan bir methodtur ve gruptan işlerin tamamlandığı bilgisini aldığında çalışır.

DispatchGroup’larla çalışırken, kullandığımız kuyruklar mevcut gruba işin bitip bitmediği hakkında bilgi verir. Ancak kuyruklar işin bitip bitmediğini her zaman doğru bir şekilde takip edemez. Örneğin bir görevin içerisinde closure ile çalıştığımızı düşünelim. Closure’lar ile çalışırken bu görev sanki tamamlanmış gibi görünür ancak arka planda çalışmaya devam eder. Bu ve bunun gibi durumlarda ilerlerken kullanabileceğimiz methodlar vardır.

Yukarıdaki örnekte enter methodu bu görevin başladığını, leave methodu ise görevin bittiğini temsil eder. Kuyruğun takip edemeyeceği durumlarda, görevin bitip bitmediğini elle belirterek işlerin doğru ilerlemesini sağlayabiliriz.

Concurrency, derin ve pratik yapılması gereken bir konu. Nerede kullanacağımı bilmiyorum diyebilirsiniz. Ancak farklı projeleri inceleyerek eşzamanlılığı nerede nasıl kullanacağımızı anlayabiliriz. Okuduğunuz için teşekkür ederim. Yorum ve önerilerinizi benimle paylaşabilirsiniz. 🙂

--

--