DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • How to Break Up Swift Concurrency
  • Swift: Master of Decoding Messy JSON
  • Unlocking the Power of Reflection in Mobile Development
  • Advanced Usage of Decodable in Swift: Handling Dynamic Keys

Trending

  • DZone's Article Submission Guidelines
  • How Docker Is Becoming an AI Development Platform
  • Java Enterprise Is Already Ready for the AI Era
  • MCP vs A2A vs ACP: How AI Agents Talk to Each Other
  1. DZone
  2. Coding
  3. Frameworks
  4. Demystifying Thread Hopping With Swift 6.2

Demystifying Thread Hopping With Swift 6.2

Swift 6.2 fixes unexpected thread hopping in async code with Approachable Concurrency. This article explains the new execution model.

By 
Nikita Vasilev user avatar
Nikita Vasilev
·
Aug. 25, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
37 Views

Join the DZone community and get the full member experience.

Join For Free

Ever since Swift Concurrency was introduced, its main mission has been clear: keep memory safe without making us write callback hell. But if we’re being honest, context switching-specifically thread hopping-has always been a bit of a head-scratcher.

How many times have you marked an async function as nonisolated on a @MainActor class, only to watch it instantly jump off to the cooperative global pool for no obvious reason?

Swift 6.2 addresses this head-on with Approachable Concurrency and its underlying flag, NonisolatedNonsendingByDefault.

Let’s break down what actually changes under the hood, how @concurrent fits into the picture, and what this all looks like when stepping through real code.

What Changes With NonisolatedNonsendingByDefault

Before Swift 6.2 (or with Approachable Concurrency turned off), any nonisolated async function would immediately yield its execution to Swift’s global cooperative executor whenever you called await. That meant constant, often unnecessary thread switching.

With Approachable Concurrency enabled (APPROACHABLE_CONCURRENCY = YES), that default behavior flips.

Ordinary async methods now behave much like their synchronous counterparts. They stay on the caller’s executor by default instead of hopping away.

A few quick rules to keep in mind:

  • nonsending: The function isn’t bound to a specific actor’s isolation domain, but it keeps the execution context of whoever called it.
  • @concurrent: The explicit opt-in attribute telling the compiler, “No, seriously, run this on the global concurrent pool.”
  • Good to know: @concurrent automatically implies nonisolated, so writing both is redundant.

Comparing the Flags: A Basic Test

Let’s look at a straightforward example to see the difference in practice:

Swift
 
@MainActor
class ViewModel {
    var title = "Hello"
    
    func updateData() {
        print("1:", Thread.isMain)
    }
    
    nonisolated func helperMethod() async {
        print("2:", Thread.isMain)
    }
    
    @concurrent
    func thirdMethod() async {
        print("3:", Thread.isMain)
    }
}

// Calling it from a MainActor context:
Task {
    let viewModel = ViewModel()
    viewModel.updateData()
    await viewModel.helperMethod()
    await viewModel.thirdMethod()
}


Quick Compiler Tip:

When testing thread execution across different isolation contexts, you might run into compiler warnings or errors when accessing Thread.isMainThread. To cleanly check the main thread without triggering actor isolation warnings, use a nonisolated helper extension:

Swift
 
extension Thread {
    static nonisolated var isMain: Bool { Thread.isMainThread }
}


Here’s what gets printed depending on your project settings:

Output APPROACHABLE_CONCURRENCY = NO APPROACHABLE_CONCURRENCY = YES
1: updateData() true true
2: helperMethod() false (Background) true (Main Thread)
3: thirdMethod() false (Background) false (Background)

What’s happening here?

  • When set to NO: Calling helperMethod() drops off the main actor and executes on a background thread (false).
  • When set to YES: helperMethod() isn’t isolated, but thanks to nonsending, it inherits the caller’s context. Since the calling Task runs on @MainActor, helperMethod() stays right there on the main thread.
  • thirdMethod() is marked @concurrent, so it always hops to a background worker thread regardless of the build setting.

Deep Dive: Following the Execution Chain

To really see how thread hopping behaves during nested calls and returns, let’s trace a slightly more complex scenario involving a custom global actor:

Swift
 
@globalActor
actor BackgroundActor {
    static let shared = BackgroundActor()
}

@MainActor
class ViewModel {
    var name = "Swift 6"
    
    // 1. Synchronous isolated method
    func runTest() {
        print("1:", Thread.isMain)
        
        Task {
            await complexHelper()
        }
    }
    
    // 2. Async nonisolated helper
    nonisolated func complexHelper() async {
        print("2:", Thread.isMain)
        
        // Jumping over to our custom actor
        await BackgroundActor.shared.doWork {
            print("3:", Thread.isMain)
        }
        
        print("4:", Thread.isMain)
        
        // Calling a sync nonisolated helper
        syncHelper()
    }
    
    // 3. Synchronous nonisolated helper
    nonisolated func syncHelper() {
        print("5:", Thread.isMain)
    }
}

extension BackgroundActor {
    func doWork(_ operation: @Sendable () -> Void) async {
        operation()
        
        Task {
            print("6:", Thread.isMain)
        }
    }
}


Side-by-Side Execution Trace:

Step APPROACHABLE_CONCURRENCY = NO APPROACHABLE_CONCURRENCY = YES
1 true true
2 false true ← Stays on caller’s thread
3 false false ← Hopped to BackgroundActor
4 false true ← Returned to caller context
5 false true ← Synchronous call from step 4
6 false false ← Task spawned inside BackgroundActor

Why steps 2, 4, and 5 change in Swift 6.2:

  • Step 2 (complexHelper): Because the caller is on @MainActor, complexHelperstarts executing on the main thread (true).
  • Step 3 (doWork): We explicitly await a method on BackgroundActor, so execution correctly hops over to a background thread (false).
  • Step 4 (After await doWork): Here’s the key difference. When doWorkfinishes, control resumes in complexHelper. Under Swift 6.2, the method remembers where it was called from, so it hops back to the Main Thread (true).
  • Step 5 (syncHelper): This is a plain synchronous call made right after step 4, so it stays on the main thread (true).

Wrapping Up

Swift 6.2’s Approachable Concurrency makes writing async Swift feel a lot more natural:

  • Fewer random context switches: Your app spends less time hopping back and forth across threads when it doesn’t need to.
  • Predictable execution: Async code holds onto its caller’s context until you explicitly use @concurrent or call into a different actor.
  • Easier mental model: Async methods now align much closer with how we expect synchronous code to flow, removing a big chunk of the concurrency learning curve.
Swift (programming language)

Published at DZone with permission of Nikita Vasilev. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • How to Break Up Swift Concurrency
  • Swift: Master of Decoding Messy JSON
  • Unlocking the Power of Reflection in Mobile Development
  • Advanced Usage of Decodable in Swift: Handling Dynamic Keys

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook