Async & Await

Swift’te Asenkron Programlamayı Basitleştiren Yapısal Yaklaşım: Async/await

Ufuk Köşker
TurkishKit
7 min readMay 11, 2024

--

Swift geliştirme süreci, clouser ve completion işlevlerini kullanarak asenkron programlama yapabiliriz. Ancak, yapıların kullanımı zorlayıcı olabilir. Neyse ki, Swift 5.5'in getirdiği yeniliklerle birlikte Xcode 13, async/await yapısını tanıtarak yapılandırılmış bir şekilde asenkron ve paralel kod yazmayı sağlayan yerleşik bir destek sundu.

Async/await yapısı, geliştiricilere clouser ve completion işlevlerini kullanmadan asenkron kod yazma imkanı sunar. Bu da Swift’teki karmaşık asenkron kodun okunabilirliğini artırırken, aynı zamanda geliştirme sürecini daha verimli hale getirir.

Async ve Await: Yeni Olanaklar

  • async: Bu anahtar kelime, bir işlevin veya özelliğin asenkron olduğunu belirtir. Kodun akışını, bir asenkron işlevin bir değer döndürmesini beklerken askıya alır.
  • await: Bu anahtar kelime, kodun bir asenkron işlevin dönüşünü beklerken yürütmesini durdurabileceğini belirtir.

Completion İşlevi yapılandırılmamışken, async-await yapısı yapılandırılmış sıralı deseni takip eder. Aşağıdaki kod örnekleri, sırasıyla clouser’ların yapılandırılmamış ve yapılandırılmış desenlerini göstermektedir.

Async-await işlevi yapılandırılmış eşzamanlılık sağlar ve bu nedenle Swift’teki karmaşık asenkron kodun okunabilirliğini artırır. Aşağıdaki kod örneğinde async-await kullanımını görebilirsiniz.

func fetchThumbnail(for url: URL) async throws -> UIImage { // 1. Yöntemi çağır
// 2. URL'den görüntüleri al ve görüntüyü döndür
let (data, response) = try await URLSession.shared.data(from: url)
// 3. Yanıt durum kodu başarılı değilse hata fırlat
guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw ARError.badID }
// 4. Yanıt verisinden Görüntü oluştur
let image = UIImage(data: data)
// 5. Görüntüyü yeniden boyutlandır ve döndür
// 6. Geçersiz görüntü ise hata fırlat
guard let thumbnail = await image?.byPreparingThumbnail(ofSize: CGSize(width: 300, height: 300)) else { throw ARError.badImage }
// 7. Geçerli görüntüyü döndür
return thumbnail
}

Aşağıda görebileceğiniz gibi, çağrı yöntemi, completion işlevi kullanarak görüntüler alınmadan önce döner. Sonuç alındığında, completion’a geri döneriz. Bu yapılandırılmamış bir işlem sırasıdır ve takip etmesi zordur. Eğer completion callback içinde başka bir asenkron yöntem gerçekleştirseydik, başka bir clouser eklenirdi. Her clouser, işlem sırasını takip etmeyi daha da zorlaştırır, çünkü her seviye için bir girinti ekler.

func fetchThumbnail(for url: URL, completion: @escaping(UIImage?, Error?) -> Void) {
// 1. Fonksiyon çağırılır.
let task = URLSession.shared.dataTask(with:url) { data, response, error in // 4. Asenkron yöntem döner

if let error = error {
// 5. Herhangi bir hata gelirse, tamamlama bloğu hatayla birlikte döner
completion(nil, error)
} else if (response as? HTTPURLResponse)?.statusCode != 200 {
// 6. Yanıt durum kodu başarılı değilse, tamamlama bloğu hatayla birlikte döner
completion(nil, ARError.badID)
} else {
guard let image = UIImage(data: data!) else {
// 7. Geçerli bir resim değilse, tamamlama bloğu hatayla birlikte döner
completion(nil, ARError.badImage)
return
}
image.prepareThumbnail(of: CGSize(width: 300, height: 300)) { thumbnail in
guard let thumbnail = thumbnail else {
// 8. Yeniden boyutlandırıldıktan sonra geçerli bir resim değilse, tamamlama bloğu hatayla birlikte döner
completion(nil, ARError.badImage)
return
}
// 9. Geçerli resim, tamamlama bloğu resim nesnesiyle birlikte döner
completion(thumbnail, nil)
}
}
}
// 2. Veri görevini devam ettir
task.resume()
}
// 3.Çağırma yöntemi çıkar

Tasks Nedir?

Tüm asenkron fonksiyonlar, bir görevin parçası olarak çalışır. Bir fonksiyon asenkron bir çağrı yaparsa, çağrılan fonksiyon hala aynı görevin bir parçası olarak çalışır (ve çağrıcı, dönmesini bekler). Benzer şekilde, bir fonksiyon asenkron bir çağrıdan döndüğünde, çağrıcı aynı görevde çalışmaya devam eder. Asenkron kod doğrudan senkron yöntemden çağrılamaz. Senkron yöntemden asenkron kodu çağırmak için, bir görev oluşturup asenkron fonksiyonu ondan çağırmamız gerekir. Bu, senkron/asenkron kod arasında bir köprü oluşturur. Görev, asenkron işleri ayrı bir iş parçacığında yürütmek ve yönetmek için bir ortam oluşturur. Görev türü aracılığıyla asenkron kodu çalıştırabilir, duraklatabilir ve iptal edebiliriz.

Task {
let resultImage = try await fetchThumbnail(for: imageURL)
... // futher work
... // take over (asynchronously)
}

Görev oluştururken, görevin ne kadar acil olduğuna bağlı olarak önceliği belirtmek önemlidir. Göreve doğrudan bir öncelik atayabilirsiniz, ancak atamazsanız Swift otomatik olarak önceliği belirlemek için üç kural izleyecektir:

  • Eğer görev başka bir görevden oluşturulmuşsa, alt görev ana görevin önceliğini miras alacaktır.
  • Eğer yeni görev doğrudan ana iş parçacığından bir görev oluşturulmuşsa, kullanıcı tarafından başlatılan en yüksek öncelik ataması otomatik olarak atanır.
  • Eğer yeni görev başka bir görev veya ana iş parçacığı tarafından oluşturulmadıysa, Swift önceliği sorgulamaya çalışacaktır.

En düşük öncelik background’dadır., burada işlem kullanıcının bakış açısından çok acil değildir. En yüksek öncelik yüksek olan, kullanıcı tarafından önemli olarak kabul edilen görevi ifade eden userInitiated ile eşanlamlıdır. Varsayılan öncelik orta düzeydedir, burada diğer işlemlerle aynı şekilde işlenir.

Eşzamanlı Olarak Birden Fazla Asenkron Fonksiyonu Çalıştırma

Eğer birden fazla ilişkisiz asenkron fonksiyonu eşzamanlı olarak çalıştırmak istiyorsak, onları kendi görevlerine sarabiliriz ve bu görevler eşzamanlı olarak çalışacaktır. Onların çalışma sırası belirsizdir.

Task(priority: .medium) {
let result1: UIImage = try await fetchThumbnail(for: profileURL)
}

Task(priority: .medium) {
let result2: UIImage = try await fetchThumbnail(for: profileThumbnailURL)
}

Task(priority: .medium) {
let result3: UIImage = try await fetchThumbnail(for: productImageURL)
}

Eşzamanlı Bağlama ile Birden Fazla Asenkron İşlemi Paralel Olarak Çalıştırma

Birden fazla asenkron işlemi paralel olarak çalıştırabilir ve sonuçları aynı anda alabiliriz. Bu, ayrıca eşzamanlı bağlama olarak da adlandırılır.

Task(priority: .medium) {
do {

async let image1 = try await fetchThumbnail(for: productImageURL1)

async let image2 = try await fetchThumbnail(for: productImageURL2)

async let image3 = try await fetchThumbnail(for: productImageURL3)

let imagesArray = try await [image1, image2, image3]

} catch {
// Handle Error
}
}

Asynchronous Sequences(Asenkron Diziler)

Swift 5.5'in yeni Asynchron sistemi, asenkron diziler ve yineleyicilerin konseptini tanıtıyor. Bir AsyncSequence, Sequence türüne benzer — adım adım ilerleyebileceğiniz bir değer listesi sunar — ve asenkronluğu ekler. Bir dizi, ilk kullandığınızda tüm, bazı veya hiçbir değeri bulunmayabilir. Bunun yerine, değerler kullanıma hazır olduğunda almak için beklemeyi kullanırsınız.

for await i in Counter(howHigh: 10) {   
print(i, terminator: " ")
}

AsyncSequence, Sequence’in asenkron bir türevidir ve AsyncSequence sadece bir protokoldür. AsyncSequence protokolü, bir AsyncIterator sağlar ve değerlerin geliştirilmesi ve potansiyel olarak depolanmasıyla ilgilenir. Asenkron yinelemeler oluştururken, AsyncSequence protokolüne uymalı ve makeAsyncIterator yöntemini uygulamalıyız.

struct ImageURL: AsyncSequence {
typealias Element = URL
var urlStr: [String]

func makeAsyncIterator() -> ImageURLData {
ImageURLData(urls: urlStr)
}
}

struct ImageURLData: AsyncIteratorProtocol {
var urls: [String]
fileprivate var index = 0

mutating func next() async throws -> URL? {
guard index < urls.count else {
return nil
}
let filePath = urls[index]
index += 1

if let strURL = URL(string: "https://image.tmdb.org/t/p/w500/\(filePath)") {
return strURL
}
return nil
}
}

private func getImages() {
let imagesFilePath = ["cOF0InT1qQVUeNjqxjF7gtEtL5L.jpg", "hT3OqvzMqCQuJsUjZnQwA5NuxgK.jpg", "8DwrHSpilQiZiegR9T8Q69ey8ru.jpg" ]

Task {
var images: [UIImage] = [UIImage]()
for try await filePath in ImageURL(urlStr: imagesFilePath) {
do {
async let (image_1) = UIImage().fetchThumbnail(for: filePath)
try await images.append(image_1)
} catch {
print(error)
}
}
}
}

Asenkron Özellikler

Bir özelliği asenkron hale getirebiliriz, bunun için özelliğin getter’ının ardından async eklememiz gerekmektedir.

extension UIImage { 
// async property
var thumbnail: UIImage? {
get async {
let size = CGSize(width: 300, height: 300)
return await self.byPreparingThumbnail(ofSize: size)
}
}
}

Sadece salt okunur özellikler asenkron olabilir, bu özellik için açıkça bir setter oluşturmamız gerekmektedir. Eğer asenkron bir özelliğe setter sağlamaya çalışırsak, derleyici bir hata verecektir.

Throws İle Asenkron Özellikler

Asenkron özellikler ayrıca throws anahtar kelimesini destekler. Bir özellik tanımında async anahtar kelimesinden sonra throws anahtar kelimesini eklememiz ve hatayı fırlatan yöntemle try await kullanmamız gerekmektedir.

extension UIImage { 
var posterImage: UIImage {
get async throws {
do {
let ( data, _) = try await URLSession.shared.data(from: imageUrl)
return UIImage(data: data)!
} catch {
throw error
}
}
}
}

Asenkron İçinde defer Kullanımı

defer bloğu, fonksiyondan çıkarken en son çalışan koddur ve kaynak temizliğinin gözden kaçırılmadığından emin olmak için yürütülmesi garanti edilir.

private func getMovies() {
defer {
print("Defer statement outside async")
}
Task {
defer {
print("Defer statement inside async")
}
let result = try? await ARMoviesViewModel().callMoviesAPIAsyncAwait(ARMovieResponse.self)
switch result {
case .failure(let error): print(error.localizedDescription)
case .success(let items): movies = items.results
case .none: print("None")
}
print("Inside the Task")
}
print("After the Task")
}
// After the Task
// Defer statement outside async
// Inside the Task
// Defer statement inside async

Continuation async APIs

Eski kod, bazı işler tamamlandığında bizi bilgilendirmek için tamamlama işleyicilerini kullanır. Ancak bunu bir asenkron işlevden kullanmak zorunda kalacağımızda — üçüncü taraf kütüphanesi veya kendi işlevlerimiz için olsun — ancak bunu asenkron hale getirmek çok çalışma gerektirebilir. Devamı sayesinde, tamamlama işleyicilerini asenkron API’lerine sarabiliriz. Kullanılabilir birkaç türde devam mevcuttur:

  • withCheckedThrowingContinuation
  • withCheckedContinuation
  • withUnsafeThrowingContinuation
  • withUnsafeContinuation
func getMoviesPostersAPI(_ movie: ARMovie, completion: @escaping PostersCompletionClosure) {
let reviewURL = checkURL(ARAPI.moviePosters, movie)
guard let url = reviewURL else {
completion(nil, ARNetworkError.invalidUrl)
return
}
ARNetworkManager().executeRequest(url: url, completion: completion)
}

func allPosters(_ movie: ARMovie) async -> (ARMoviePoster?, Error?) {
await withCheckedContinuation { continuation in
getMoviesPostersAPI(movie) { posters, error in
continuation.resume(returning: (posters, error))
}
}
}

Devamı yalnızca tam olarak bir kez devam ettirilmelidir. Sıfır kez değil, iki veya daha fazla kez değil tam olarak bir kez.

async-await’ın Avantajları

  • İç içe geçmiş kapanışlarla oluşan “Doom Piramidi” probleminden kaçınma
  • Kodun azaltılması
  • Daha okunabilir olması
  • async/await ile güvenlik, bir sonuç garantilenirken tamamlama blokları çağrılabilir veya çağrılmayabilir.

Sonuç

Swift 5.5 ve Xcode 13 ile gelen async/await yapısı, Swift geliştirme sürecini daha kolay ve verimli hale getiriyor. Yapılandırılmış asenkron programlama sayesinde, karmaşık asenkronik kodlar daha okunabilir ve yönetilebilir hale geliyor. Bu da geliştiricilere daha etkili ve güçlü uygulamalar oluşturma imkanı sunuyor.

Bizi daha yakından takip etmek istiyorsanız, sosyal medya hesaplarımıza aşağıdan ulaşabilirsiniz!

Twitter | Instagram | Facebook

--

--