Swift, crypto

iSLa

Newbie
Joined
5 Nov 2025
Messages
3
Reaction score
0
Points
1
Threads and Tasks in Swift Concurrency
Concurrent programming has always been tough. For years, developers had to wrangle with threads, mutexes, semaphores, and a bunch of low-level constructs. Grand Central Dispatch (GCD) made things better, but it still felt wild—sometimes it worked, sometimes it bit.

Swift Concurrency fundamentally changes everything. Instead of working directly with threads, now you work with tasks. It’s no longer “which thread is this running on?” but “what should be done?” The async/await model, since Swift 5.5, makes code a lot cleaner and safer for parallel operations.

Instead of spinning up a thread for every task, Swift’s system schedules execution on a thread pool. This avoids the *thread explosion* problem—apps creating more threads than the device can handle, which destroys performance. Swift Concurrency isn’t just a thin wrapper around GCD; it has its own task scheduler and yields threads at suspension points for efficiency.

Quick comparison (Old vs. New):

Code:
 swift


// Old approach with GCD


DispatchQueue.global().async {


    let data = loadDataFromNetwork()


    DispatchQueue.main.async {


        self.updateUI(with: data)


    }


}





// New approach with Swift Concurrency


Task {


    let data = try await loadDataFromNetwork()


    await MainActor.run {


        self.updateUI(with: data)


    }


}


After months of using Swift Concurrency: cleaner code, far fewer headaches with race conditions and memory leaks. Debugging deadlocks—barely missed it.


Evolution of Multithreading in iOS

Back in the early days—NSThread and NSOperation ruled.


Code:
 swift


let thread = Thread {


    let image = self.processLargeImage()


    Thread.main.execute {


        self.imageView.image = image


    }


}


thread.start()

Threads were expensive: each had its own stack and kernel resources. Creating too many led to “thread explosion”—bad performance, crashes, especially on mobile.

NSOperation/NSOperationQueue offered better abstractions, letting you manage dependencies between operations.

GCD Era





GCD simplified async programming:


Code:
 swift


// Run heavy task in background


DispatchQueue.global(qos: .userInitiated).async {


    let result = heavyComputation()


    // Update the UI on the main thread


    DispatchQueue.main.async {


        self.updateUI(with: result)


    }


}








But… callback hell reared its head:


Code:
 swift


DispatchQueue.global().async {


    self.loadUserData { userData in


        self.loadFriends(for: userData) { friends in


            self.loadPhotos(for: friends) { photos in


                let processedPhotos = self.processPhotos(photos)


                DispatchQueue.main.async {


                    self.updateUI(with: processedPhotos)


                }


            }


        }


    }


}

Error handling made this even worse. Seven levels of nested callbacks? Been there.

Memory Leaks: The Trap with GCD and ARC

Code:
 swift

class NetworkManager {


    func fetchData() {


        DispatchQueue.global().async {


            let data = self.heavyNetworkRequest()


            DispatchQueue.main.async {


                self.processData(data)


            }


        }


    }


}
That closure captures a strong reference to `self`—potential for leaks if the object is deallocated. Solution: weak references.

Code:
 swift

DispatchQueue.global().async { [weak self] in


    guard let self = self else { return }


    let data = self.heavyNetworkRequest()


    DispatchQueue.main.async { [weak self] in


        guard let self = self else { return }


        self.processData(data)


    }


}





Cumbersome, error-prone, and easy to forget.











Traditional Problems





- Race conditions: Data unpredictably modified on different threads.


- Deadlocks: Threads waiting on resources held by each other, stuck forever.


- Thread explosion: Too many threads, bogging down devices.


- Priority inversion: Low-priority queues block high-priority tasks.


- Cancellation: No easy way to cancel running async operations.


When GCD Shows Its Limits


As APIs got fancier:

- Declarative UI (SwiftUI): Needed structured concurrency, not GCD spaghetti.

- Reactive programming (Combine): Imperative GCD didn’t mesh.

- Complex networking: Many requests = ugly nested closures.

Swift Concurrency solves these by giving structured, safer, and more predictable async code.

Promise Libraries (Temporary Fix)

PromiseKit helped clean up nesting:


Code:
 swift


firstly {


    fetchUserProfile()


}


.then { profile in


    fetchUserFriends(profile.id)


}


.then { friends in


    fetchFriendsPhotos(friends)


}


.done { photos in


    self.updateUI(with: photos)


}


.catch { error in


    self.handleError(error)


}

Nicer, but didn’t fix the underlying issues of threading, priorities, or cancellation.


Reactive Revolution: RxSwift, Combine


Declarative handling of events:

Code:
 swift


fetchUser()


    .flatMap { user in fetchFriends(for: user) }


    .flatMap { friends in fetchPhotos(for: friends) }


    .observeOn(MainScheduler.instance)


    .subscribe(


        onNext: { photos in self.updateUI(with: photos) },


        onError: { error in self.handleError(error) }


    )


    .disposed(by: disposeBag)


But debugging was tough, and problems below the surface stuck around.

Threads vs. Tasks


Thread: A system resource for running instructions. Expensive to create and switch.

Code:
 swift


let thread = Thread {


    performHeavyCalculation()


    print("Calculation completed")


}


thread.start()


 





 Task:  High-level unit of asynchronous work. Not bound to a specific resource—runs in Swift’s thread pool.





Code:
 swift


Task {


    let result = await performHeavyCalculation()


    print("Calculation completed with the result: \(result)")


}


Tasks are lightweight and scheduled dynamically across a limited thread pool.











Structured Concurrency





Hierarchy for async ops—parent controls lifecycle of children.





Code:
 swift


func processUserData() async throws {


    try await withThrowingTaskGroup(of: Void.self) { group in


        group.addTask {


            try await self.downloadUserProfile()


        }


        group.addTask {


            try await self.downloadUserSettings()


        }


        try await group.waitForAll()


    }


    print("All user data has been loaded")


}





If the parent cancels, so do all children—clean resource management, no accidental background work after a screen closes.

Ownership and Memory Management

GCD forced manual rules for context capture:

Code:
 swift


someQueue.async { [weak self] in


    guard let self = self else { return }


    // Use self


}


Swift Concurrency: compiler handles captures, avoiding leaks and circular references.











Explicit Cancellation





Cancellation built in—one call cancels whole hierarchies:





Code:
 swift


let task = Task {


    try await performLongOperation()


}


// somewhere else:


task.cancel()


Check for cancellation in functions:





Code:
 swift


func performLongOperation() async throws {


    for i in 1...1000 {


        try Task.checkCancellation()


        await processChunk(i)


    }


}











Cooperative Threading





Swift maintains a small pool of threads (about as many as CPU cores).





Code:
 swift


struct ThreadingDemonstrator {


    func demonstrate() {


        Task {


            try? await firstTask()


        }


        Task {


            await secondTask()


        }


    }





    private func firstTask() async throws {


        print("Task 1 started on thread: \(Thread.current)")


        try await Task.sleep(for: .seconds(2))


        print("Task 1 resumed on thread: \(Thread.current)")


    }





    private func secondTask() async {


        print("Task 2 started on thread: \(Thread.current)")


    }


}





Tasks can start and resume on different threads—major improvement over the old blocking model.











Task Scheduling and Priorities





Scheduler uses:





- Task priorities


- Data locality


- Thread availability





Code:
 swift


// High priority


Task(priority: .high) {


    await highPriorityWork()


}





// Standard priority


Task {


    await standardPriorityWork()


}





Suspension Points





Phases in async functions where execution can pause:





Code:
 swift


func processImage() async throws -> UIImage {


    let data = try await downloadImageData()   // Pause 1


    let image = try await decodeImage(data)    // Pause 2


    let processedImage = try await applyFilters(to: image)  // Pause 3


    return processedImage


}


Swift compiles these into state machines for efficient context switching.











MainActor for UI Updates


Code:
 swift


// Old


DispatchQueue.global().async {


    let data = processData()


    DispatchQueue.main.async {


        self.updateUI(with: data)


    }


}





// New


Task {


    let data = await processData()


    await MainActor.run {


        updateUI(with: data)


    }


}





All UI changes run on the main thread, keeping apps smooth and responsive.











Migration Notes





Switching from GCD to Swift Concurrency? Watch out for these mistakes:





- Thinking in threads instead of tasks.


- Creating `Task` inside async methods—redundant.


- Not managing task lifecycles/cancellation.


- Ignoring built-in cancellation.





Callback hell refactoring: use async functions and continuations.





Enjoy!