Yazılarımız

Veri Akademi

SWİFT ASYNC/AWAİT NEDİR? CONCURRENCY, TASK VE ACTOR MANTIĞI

iOS uygulamalarında “akıcı arayüz” ile “hızlı veri” arasındaki savaş hiç bitmez: ağ istekleri, dosya işlemleri, veritabanı sorguları ve ağır hesaplamalar aynı anda çalışmak ister. Swift’in async/await yaklaşımı, bu karmaşayı daha okunur bir akışa çevirerek hem performansı hem de bakım kolaylığını artırır.

Eskiden completion handler zincirleri, GCD kuyrukları ve “callback hell” ile boğuşurken, artık asenkron adımlarınızı sanki senkron kod yazıyormuş gibi anlatabilirsiniz. Üstelik bu yalnızca söz dizimi kolaylığı değildir: Swift Concurrency’nin arkasında structured concurrency, iptal (cancellation) propagasyonu ve veri yarışlarını azaltan actor modeli gibi güçlü ilkeler bulunur.

Bu yazıda, “Swift async/await nedir?” sorusunu pratik bir çerçeveye oturtacağız: Concurrency’nin temel kavramları, Task kullanımı, actor mantığı, hata/iptal yönetimi ve günlük iOS geliştirmede uygulanabilir tasarım ipuçlarıyla konuyu uçtan uca ele alacağız.

Xcode’da asenkron fonksiyon akışını gösteren Swift kodu ve ağ isteği senaryosu, okunabilirlik odaklı bir yapı

Swift Concurrency’ye hızlı bakış: Neyi çözüyor?

Swift Concurrency, asenkron programlamayı derleyici ve çalışma zamanı seviyesinde destekleyen bir modeldir. Buradaki hedef; aynı anda birden fazla iş yürütürken kodun okunabilirliğini korumak, iptali doğal hale getirmek ve özellikle paylaşılan durum (shared state) kaynaklı hataları azaltmaktır.

Bu yaklaşım, yalnızca “arka planda çalıştır, bitince dön” kolaylığı sunmaz. Asıl fark; işlerin yaşam döngüsünün daha iyi tanımlanması, görev hiyerarşisi (parent-child ilişkisi) ve güvenli veri erişimi gibi konularda ortaya çıkar.

Neden async/await? Okunabilirlik ve akış kontrolü

Completion handler’lar ile ilerleyen bir kodda, hata yönetimi, erken dönüşler ve ardışık adımlar çoğunlukla iç içe bloklara dönüşür. async/await ise “önce şunu yap, sonra bunu bekle, ardından sonucu işle” akışını düz bir yapı gibi ifade etmenizi sağlar. Bu, ekip içinde kod incelemesini hızlandırır ve regresyon riskini düşürür.

GCD ve callback mirası: Hangi noktalarda zorlanıyorduk?

DispatchQueue ile iş bölmek hâlâ mümkün olsa da, karmaşık akışlarda şu problemler sık görülür: yanlış kuyruğa dönüş, unutulan main thread güncellemesi, iptal edilemeyen operasyonlar, iç içe closure’lar ve paylaşılan değişkenlere eşzamanlı erişim. Swift Concurrency, bu sorunları sistematik şekilde ele alır ve derleyicinin bazı hataları daha erken yakalamasını sağlar.


async ve await sözdizimi: Temel yapı taşları

async/await iki parçalı bir anlaşmadır: Bir fonksiyon async ise içinde bekleme (suspend) noktaları olabilir; çağıran taraf da bu fonksiyonu await ile çağırarak “sonucu gelene kadar burada bekle” der. Buradaki bekleme, thread’i bloke etmek anlamına gelmez; görev askıya alınır ve sistem kaynakları verimli kullanılır.

async fonksiyon imzası: Derleyicinin sözleşmesi

Bir fonksiyonu async yapmak, onun asenkron bağlamda çalışacağını ve potansiyel olarak askıya alınabileceğini belirtir. Çoğu zaman ağ istekleri, dosya okuma/yazma veya uzun süren hesaplamalar için ideal bir yaklaşımdır.

await: Suspend noktaları ve kontrolün geri verilmesi

await kullandığınız satır, bir “suspend point” olarak düşünülebilir. Bu noktada görev durabilir, başka işler çalışabilir, sonra kaldığı yerden devam edebilir. Bu sayede UI donmaz, ancak siz yine de kodu düz bir anlatımla yazabilirsiniz. Eğer bir fonksiyon hata da fırlatıyorsa, async throws şeklinde tanımlanır ve çağırırken try await kullanılır.

import Foundation

struct User: Decodable {
    let id: Int
    let name: String
}

enum APIError: Error {
    case invalidURL
    case badStatus(Int)
}

final class UserService {
    private let session: URLSession = .shared

    func fetchUser(id: Int) async throws -> User {
        guard let url = URL(string: "https://example.com/users/\(id)") else {
            throw APIError.invalidURL
        }

        let (data, response) = try await session.data(from: url)

        if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
            throw APIError.badStatus(http.statusCode)
        }

        return try JSONDecoder().decode(User.self, from: data)
    }
}

@MainActor
final class ProfileViewModel {
    private let service = UserService()
    private(set) var userName: String = ""

    func load(id: Int) async {
        do {
            let user = try await service.fetchUser(id: id)
            userName = user.name
        } catch {
            userName = "Bilinmeyen kullanıcı"
        }
    }
}

Örnekte dikkat edilmesi gereken nokta, UI ile ilişkili state’in @MainActor altında güncellenmesidir. Böylece arayüz güncellemesi için yanlış thread’e düşme riskini azaltırsınız.


Task ve Structured Concurrency: İşleri doğru şekilde organize etmek

Swift Concurrency’de en önemli kavramlardan biri structured concurrency’dir. Bu yaklaşım, görevlerin yaşam döngüsünü belirginleştirir: Bir task oluşturduğunuzda onun ebeveyni, iptal durumu ve kapsamı daha net olur. “Başlat ve unut” tarzı kontrolsüz eşzamanlılık yerine, daha izlenebilir bir model benimsenir.

Task { } ne işe yarar? UI’dan asenkron dünyaya geçiş

Genellikle bir buton aksiyonu veya yaşam döngüsü olayı içinde asenkron bir fonksiyon çağırmak istersiniz. Senkron bir bağlamdan async fonksiyona geçiş yapmak için Task kullanılır. Bu, mevcut bağlamda yeni bir görev başlatır.

Task.detached: Ne zaman kaçınmalı?

Task.detached, ebeveyn bağlamından kopuk bir görev başlatır. Bu, iptal propagasyonu ve actor context gibi avantajları kaybetmenize neden olabilir. Çoğu uygulama senaryosunda Task veya TaskGroup gibi yapılandırılmış seçenekler daha güvenlidir. Detached kullanımı daha çok bağımsız, uygulama genelinde sürmesi gereken işler için düşünülmelidir; yine de dikkatli bir tasarım gerekir.

TaskGroup ile paralel iş yürütme

Birden fazla bağımsız isteği aynı anda yapmak istiyorsanız TaskGroup güçlü bir seçenektir. Örneğin profil, ayarlar ve bildirim sayısı gibi verileri paralel çekip sonuçları birleştirebilirsiniz. Burada amaç, paralelliği “yönetilebilir” hale getirmektir.

  • Bağımsız istekleri aynı anda başlatmak
  • Sonuçları tek bir akışta toplayıp işlemek
  • Hata veya iptal durumunda tutarlı davranmak
  • UI güncellemelerini ana aktöre taşımak
Birden fazla isteği paralel çalıştıran TaskGroup akışı, sonuçların tek noktada birleştirilmesi ve iptal davranışı

TaskGroup kullanırken, her alt görevin kapsamını iyi tanımlamak önemlidir. Aşırı paralellik, ağ ve CPU kaynaklarını tüketebilir; doğru dengeyi bulmak için öncelik ve kapsam yaklaşımı faydalıdır.


Actor mantığı: Veri yarışlarını azaltmanın anahtarı

Eşzamanlı programlamada en sinsi sorunlardan biri “data race” olarak bilinen veri yarışlarıdır: İki farklı iş aynı veriyi aynı anda okumaya/yazmaya çalışır ve tutarsız sonuçlar üretir. Swift’in actor modeli, paylaşılan durumun tek bir izolasyon alanında yönetilmesini hedefler.

Actor izolasyonu: Paylaşılan state tek kapıdan geçsin

Actor içindeki değişkenlere erişim, actor’un izolasyon kurallarına tabidir. Yani dışarıdan bir actor state’ini okurken veya değiştirirken genellikle await kullanmanız gerekir. Bu, eşzamanlı erişimi kontrollü hâle getirir ve kilit (lock) karmaşasına daha az ihtiyaç duyarsınız.

MainActor: UI güncellemeleri için net bir sınır

iOS’ta UI güncellemeleri ana thread üzerinde yapılmalıdır. Swift Concurrency bu ihtiyacı @MainActor ile netleştirir. ViewModel’inizi veya UI ile temas eden servislerinizi MainActor ile işaretlemek, yanlış yerden UI güncelleme riskini azaltır ve kodun niyetini belirginleştirir.

import Foundation

actor TokenCache {
    private var token: String?
    private var expiresAt: Date?

    func getValidToken() -> String? {
        guard let token, let expiresAt else { return nil }
        return expiresAt > Date() ? token : nil
    }

    func save(token: String, ttlSeconds: TimeInterval) {
        self.token = token
        self.expiresAt = Date().addingTimeInterval(ttlSeconds)
    }
}

@MainActor
final class AuthViewModel {
    private let cache = TokenCache()
    private(set) var statusText: String = "Hazır"

    func refreshIfNeeded() async {
        if let existing = await cache.getValidToken() {
            statusText = "Token geçerli: \(existing.prefix(6))…"
            return
        }

        // Örnek: ağdan token çektiğinizi düşünün
        do {
            try await Task.sleep(nanoseconds: 400_000_000)
            let newToken = UUID().uuidString
            await cache.save(token: newToken, ttlSeconds: 60 * 10)
            statusText = "Token yenilendi"
        } catch {
            statusText = "Yenileme başarısız"
        }
    }
}

Burada actor, token ile ilgili state’i güvenli bir kapıya alırken, ViewModel UI metnini MainActor altında günceller. Böylece sorumluluklar ayrılır ve kod daha sürdürülebilir olur.


Cancellation, hata yönetimi ve zaman aşımı

Asenkron akışlarda yalnızca “başlat ve bitir” yoktur; kullanıcı ekranı kapatabilir, istek gereksiz hâle gelebilir veya sistem kaynakları kısıtlanabilir. Swift Concurrency’nin cancellation modeli, bu durumları daha doğal yönetmenizi sağlar. Ayrıca hata yönetimi, async/await ile daha düzenli bir akışa oturur.

Cancellation nasıl çalışır? İptal propagasyonu ve kontrol noktaları

Bir task iptal edildiğinde, alt görevlerine de iptal sinyali taşınabilir. Bu, özellikle structured concurrency ile anlam kazanır. İptal her zaman otomatik olarak “dur” demeyebilir; sizin de bazı noktalarda iptal kontrolü yapmanız gerekir. Örneğin, uzun süren bir döngüde periyodik iptal kontrolü mantıklı olabilir.

async throws ve try await: Hata akışını sadeleştirmek

async fonksiyonlar hata fırlatabiliyorsa, çağıran taraf try await ile hem beklemeyi hem de hata yakalamayı aynı akışta yapar. Böylece “error callback’i” ile “success callback’i” arasında bölünmüş mantık yerine, tek bir do-catch bloğunda net bir hikâye yazarsınız.

Zaman aşımı: Sonsuza kadar beklemeyi engellemek

URLSession bazı durumlarda uzun süre bekleyebilir. Uygulamanız için kabul edilebilir bir süre belirlemek; kullanıcı deneyimi, pil tüketimi ve kaynak yönetimi açısından faydalıdır. Zaman aşımı yaklaşımını; task iptali, URLSessionConfiguration timeout’ları veya kendi timeout sarmalayıcınız ile kurgulayabilirsiniz.

  1. Asenkron işin kapsamını netleştirin ve iptal ihtimalini tasarıma dahil edin
  2. UI’dan başlatılan görevleri ekran yaşam döngüsüyle uyumlu yönetin
  3. Hata türlerini (network, decode, status code) ayırarak anlamlı mesajlar üretin
  4. Gereksiz uzun beklemeleri timeout yaklaşımıyla sınırlayın
İptal edilen bir görev akışında hata yakalama, zaman aşımı ve kullanıcı etkileşimi ile kontrolün geri kazanılması senaryosu

Pratik tasarım ipuçları: Performans, test edilebilirlik ve mimari

async/await’i doğru kullanmak, sadece söz dizimini öğrenmekle bitmez. Uygulamanın mimarisi, bağımlılık yönetimi ve test stratejisi de concurrency kararlarınızı etkiler. Burada amaç; doğru yerde eşzamanlılık ve doğru yerde izolasyon sağlamaktır.

Öncelik, kaynak yönetimi ve ölçümleme

Her işi paralelleştirmek iyi sonuç vermez. Özellikle mobil cihazlarda CPU, ağ ve bellek kaynakları sınırlıdır. Ağ isteklerini gereksiz çoğaltmak yerine cache stratejisi kurmak; ağır hesaplamaları uygun zamanda ve uygun öncelikle yürütmek daha dengeli bir yaklaşım sağlar. Instruments ile ağ ve CPU profili almak, gerçek darboğazı bulmanın en pratik yoludur.

Test edilebilirlik: Servisleri protocol ile soyutlamak

Asenkron servisleri test etmek için bağımlılıkları protocol üzerinden soyutlamak, mock implementasyonlarla deterministik senaryolar kurmanıza yardım eder. async fonksiyonlar, XCTest içinde asenkron test akışlarına daha rahat oturur; böylece “callback bekleme” karmaşası azalır.

Actor kullanımında sınırlar: Her şeyi actor yapmak gerekir mi?

Actor’lar, paylaşılan state için güçlü bir araçtır; ancak her sınıfı actor’a çevirmek gereksiz karmaşıklık yaratabilir. Tipik bir kural: Paylaşılan ve eşzamanlı erişilen state varsa actor düşünün; yoksa daha sade yapıları tercih edin. Bir cache, sayaç, token deposu veya event toplama mekanizması actor için iyi örneklerdir.

UI ile entegrasyon: ViewModel’de Task yönetimi

ViewModel içinde başlatılan task’leri gerektiğinde iptal edebilmek önemlidir. Örneğin kullanıcı arama ekranında yazı yazdıkça yeni istek atılıyorsa, eski görevleri iptal etmek hem hız hem de maliyet açısından kritik olabilir. Bu gibi senaryolarda task referansını saklayıp iptal etmek, daha kontrollü bir deneyim sağlar.

Eğer Swift ve iOS tarafında concurrency dahil daha geniş bir müfredatla ilerlemek isterseniz, kapsamlı içerikler için iOS Swift eğitimi sayfasına göz atabilirsiniz; async/await pratiklerini mimari örneklerle birlikte pekiştirmek öğrenmeyi hızlandırır.


Özet: Swift async/await ile daha güvenli ve okunur eşzamanlılık

Swift’in async/await yaklaşımı, asenkron kodu düz bir akışta ifade ederek okunabilirliği artırır; Task ve structured concurrency ile görev yaşam döngüsünü daha yönetilebilir hâle getirir; actor mantığıyla paylaşılan state kaynaklı veri yarışlarını azaltır. İptal ve hata yönetimi gibi gerçek dünya ihtiyaçları da modelin içine entegre olduğu için, uygulamanız büyüdükçe avantajı daha görünür olur.

Gün sonunda hedef; “daha fazla paralellik” değil, doğru yerde doğru eşzamanlılık kurmaktır. async/await’i Task, TaskGroup, MainActor ve actor izolasyonuyla birlikte düşündüğünüzde, hem performans hem de sürdürülebilirlik açısından daha sağlam bir iOS kod tabanı elde edersiniz.

 VERİ AKADEMİ