When learning Swift Concurrency, one of the hardest mental shifts is letting go of the idea that you control which thread your code runs on. If you’ve worked with Grand Central Dispatch or NSOperationQueues, you’re probably used to thinking:

I need this code on the main thread

Or similar:

“This work should go to a background thread”

But Swift Concurrency invites you to trust the system to manage threads for you. It’s designed to make optimal decisions for performance, responsiveness, and correctness—without needing you to micromanage threads.

The previous lesson set the tone and showed you why letting Swift Concurrency manage threads is a good thing. Preventing high memory overhead, excessive context switching, and priority inversion issues should be reason enough to say goodbye to manual thread management.

Let’s explore what this shift looks like, and why it matters.

Think in isolation domains, not threads

Instead of thinking “what thread should this run on?”, think:

“What context or actor should own this work?”

Or even better in other words: which isolation domain should this code perform?

Actors in Swift provide isolated regions of memory where data is safe from data races. You don’t assign code to a thread; you assign code to an actor, and Swift ensures that work is executed safely and serially in that isolation domain.

You can basically say you’re instructing Swift Concurrency to run code as efficiently as possible on any thread, as long as it performs inside the given isolation domain. This makes your code more robust because Swift can optimize thread usage without violating safety.

Provide hints using task priorities

Similarly, you no longer directly pick a queue’s quality-of-service (QoS) or priority. Instead, you give Swift a priority hint:

Task(priority: .userInitiated) {
    await doImportantWork()
}

This communicates “this task is important to the user”—and lets the system choose the best execution strategy. Again: you’re not assigning it to a thread; you’re describing the nature of the work. You’re once again trusting Swift Concurrency to do its work behind the scenes.

While we’ve discussed priorities in detail in module 3, I want to emphasize again that we’re just giving hints here. A high-priority task cannot be guaranteed to run directly—it might be waiting for a low-priority task that’s already performing work. Yet, we trust Swift Concurrency to run it as soon as possible.

Why losing the threading mindset makes it clearer why we need Sendable conformance

By giving Swift Concurrency control over thread management, you lose guarantees about what thread your code will run on. This is intentional: the system moves work between threads for efficiency. This also means your code can be transferred between threads while it seems to be performing in the same chronological order.

To remind you about this, I’d like to bring back last lesson’s code example:

struct ThreadingDemonstrator {
    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)")
    }

    func demonstrate() {
        Task {
            try await firstTask()
        }
        Task {
            await secondTask()
        }
    }
}

When we call demonstrate(), we might see something like:

Task 1 started on thread: <NSThread: 0x600001752200>{number = 3, name = (null)}
Task 2 started on thread: <NSThread: 0x6000017b03c0>{number = 8, name = (null)}
Task 1 resumed on thread: <NSThread: 0x60000176ecc0>{number = 7, name = (null)}

Notice that task 1 performs work on multiple threads. Even though we’re in the same asynchronous method, we’re still performing work on multiple threads. This is why, even inside a single method, you might need to make types conform to Sendable. Obviously, Swift is smart enough to only require you to do so if it detects a change in isolation domains.

Work migrated between threads, and this is normal in Swift Concurrency. It balances work across threads behind the scenes. So, a key insight: since you can’t predict which thread code will resume on, you need to ensure that any data shared across concurrency boundaries is safe to send between threads.

And that’s what Sendable enforces: it guarantees that a type’s values can safely cross concurrency boundaries.

Summary

Let go of thread micromanagement and trust Swift to manage threads for you. Provide instructions by setting priorities or configuring isolation domains. Make your types conform to Sendable to ensure they can safely be used across concurrency boundaries, since you cannot know which thread your code will execute on.

In the next lesson, we’ll dive into so-called suspension points.