- 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):
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.
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:
But… callback hell reared its head:
Error handling made this even worse. Seven levels of nested callbacks? Been there.
Memory Leaks: The Trap with GCD and ARC
That closure captures a strong reference to `self`—potential for leaks if the object is deallocated. Solution: weak references.
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:
Nicer, but didn’t fix the underlying issues of threading, priorities, or cancellation.
Reactive Revolution: RxSwift, Combine
Declarative handling of events:
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.
Tasks are lightweight and scheduled dynamically across a limited thread pool.
Structured Concurrency
Hierarchy for async ops—parent controls lifecycle of children.
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:
Swift Concurrency: compiler handles captures, avoiding leaks and circular references.
Explicit Cancellation
Cancellation built in—one call cancels whole hierarchies:
Check for cancellation in functions:
Cooperative Threading
Swift maintains a small pool of threads (about as many as CPU cores).
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
Suspension Points
Phases in async functions where execution can pause:
Swift compiles these into state machines for efficient context switching.
MainActor for UI Updates
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!
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)
}
}
}
}
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!
