Sending 'geoRegion' risks causing data races

I have this simple piece of code that of course correctly ran in Swift 5:

func geoRegion()-> CLRegion?{
        guard let location=referenceLocation else{
            return nil
        }
        return CLCircularRegion(center:location.coordinate, radius:50000, identifier:"georeferencing")
}
    
func placemarksForAddress(_ address: String) async throws  -> [CLPlacemark]?{
        if let placemark=placemarkCache[address]{
            if placemark.location!.distance(from: referenceLocation!)<100000{
                return [placemark]
            }
        }
        
        do{
            guard let geoRegion=self.geoRegion() else {
                return nil
            }
            let placemarks = try await georeferenceQueue.geocodeAddressString( address, in: geoRegion)
            if placemarks.count>=0{
                self.placemarkCache[address]=MKPlacemark(placemark: placemarks[0])
                return placemarks
            }
        } catch {
            let placemarks=try await self.placemarkForLocation(referenceLocation)
            return placemarks
        }
        return nil
}

That now presents error:

Sending task-isolated 'geoRegion' to actor-isolated instance method 'geocodeAddressString(_:in:)' risks causing data races between actor-isolated and task-isolated uses

Answered by DTS Engineer in 887153022

fbartolom wrote:

As a matter of fact I moved everything in the actor and the error went away.

Cool. I’m glad you got this resolved.

ps It’s better to reply as a reply, rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.


none of the classes defined in any Apple API are actually sendable

Hmmmm, that’s a bit extreme. There are plenty of classes that are sendable in our various platform SDKs.

For the ones that aren’t, there are two common reasons:

  • The class really isn’t sendable.
  • The class is probably sendable but hasn’t been annotated as such.

Sadly, it’s hard to distinguish these cases. Which makes things tricky because the available paths forward vary by case:

  • If the class really isn’t sendable, your only good option is, as you say, extract the data from the class into your own type that is sendable, send that, and then, if necessary, reconstruct the class instance on the other side.
  • In the other case, you also have the option of doing something unsafe.

Honestly, I tend to favour the first option is almost all situations.

Looking at CLRegion, on first glance it looks like it should be sendable. However, this is Objective-C, and thus subclasses are a concern. And the CLBeaconRegion looks a lot less sendable than its superclass (note those non-atomic properties!)

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

It would be helpful to know which line exhibits the actual error.

But generally speaking, none of the classes defined in any Apple API are actually sendable. That's why Apple pushes structs so heavily.

This is a bit of a catch-22. It isn't the first time Apple rolled out some cool new feature but then didn't support it in their APIs.

Supposedly, there is some new "sending" modifier, but it does't seem to work.

I recommend using Apple data structures only for Apple APIs. Create your own data structures for your data and use that in your code. You can make them Sendable by any means necessary. But then, when you need to give them to an Apple API, add a little function that will export them to the non-sendable Apple class that the API expects.

fbartolom wrote:

As a matter of fact I moved everything in the actor and the error went away.

Cool. I’m glad you got this resolved.

ps It’s better to reply as a reply, rather than in the comments; see Quinn’s Top Ten DevForums Tips for this and other titbits.


none of the classes defined in any Apple API are actually sendable

Hmmmm, that’s a bit extreme. There are plenty of classes that are sendable in our various platform SDKs.

For the ones that aren’t, there are two common reasons:

  • The class really isn’t sendable.
  • The class is probably sendable but hasn’t been annotated as such.

Sadly, it’s hard to distinguish these cases. Which makes things tricky because the available paths forward vary by case:

  • If the class really isn’t sendable, your only good option is, as you say, extract the data from the class into your own type that is sendable, send that, and then, if necessary, reconstruct the class instance on the other side.
  • In the other case, you also have the option of doing something unsafe.

Honestly, I tend to favour the first option is almost all situations.

Looking at CLRegion, on first glance it looks like it should be sendable. However, this is Objective-C, and thus subclasses are a concern. And the CLBeaconRegion looks a lot less sendable than its superclass (note those non-atomic properties!)

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hmmmm, that’s a bit extreme. There are plenty of classes that are sendable in our various platform SDKs.

Sorry. Perhaps I should have qualified that statement to make it clear I was talking about a general type of behaviour rather than a mathematical absolute.

What I meant to say is that sometimes Apple APIs are simply incompatible with standard software development techniques and architectures. The example I was thinking about at the time was Objective-C exceptions.

Another example is multithreaded code. Most Apple APIs, with very few exceptions (the general kind, no the try/catch kind), should only be called on the main thread. And even the few documented thread-safe APIs aren't always safe. Swift finally seems to have solved that via Approachable Concurrency.

But now that I think about it, it seems there is yet another fundamental incompatibility. This likely explains the problem I had trying to use "sending". Swift concurrency is incompatible with object-oriented programming. Simple demos work fine because they're simple functions. But a complicated, real-world app is always going to have "self" objects. That's where the problem comes in.

I think the OP in this case was hitting this problem too. They moved code to an actor to fix it. But that's not a solution in all cases.

There are many cases where I want to have a "sending" behaviour which isn't possible in OO. For example, at some point in applicationDidFinishLaunching or viewDidLoad, I have the information needed to start a timer or other long-running task. I can't do that because self isn't sendable, and isn't ever going to be. And yet, it's still safe because Approachable Concurrency requires Task/await with sendable data from the concurrent thread to call back into the main thread.

What I've done is add "@unchecked Sendable" to the higher-level class (a shared Model rather than a view in this case). The cognitive effort required to ensure that all public and/or concurrent access is safe via os_unfair_lock is less than trying to deal with Swift's "data race" errors.

Ultimately, I think the question does come back to mathematics. I don't think the word "safe" is really the binary that Apple wants to claim. Driving isn't safe, so I wear seatbelts, but not a crash helmet. Is that "unsafe"? Maybe. But sometimes I need to go places and there's always a sweet spot between absolute safety and practicality. Programming's the same way.

Most Apple APIs, with very few exceptions … should only be called on the main thread.

I don’t agree with that summary. Ignoring Swift concurrency, I generally group Apple’s Cocoa APIs into four categories:

  • Main thread — These are limited to the main thread, with occasional exceptions. The canonical example of this is AppKit.
  • Thread safe — These have internal locking and can be used from any thread. A good example is NSOperationQueue.
  • Thread or queue confined — These must be used from a specific thread (or queue) that’s not necessarily the main thread (or queue). NSRunLoop is a good example of this.
  • Serialisation required — These have no internal locking but can be used from arbitrary threads as long as you serialise that access. A good example is NSMutableDictionary.

Your summary doesn’t include the last two categories, and IME that’s the biggest group of traditional Cocoa APIs.

Unfortunately it’s hard to distinguish between these groups because Objective-C has no concurrency annotations. You have to rely on the documentation, which is less than ideal. This is the problem that Swift concurrency is trying to solve.

I can't do that because self isn't sendable, and isn't ever going to be.

I agree that self can’t be sendable, but that doesn’t prevent you from doing this. You just have to disentangle your non-main-thread code from your main-thread code.

Consider this:

import Cocoa

@concurrent
func calculateTheAnswer(_ f1: Int, _ f2: Int) async -> Int {
    sleep(30)
    return 42
}

@main
final class AppDelegate: NSObject, NSApplicationDelegate {

    var theAnswer: Int = 0

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let f1 = 6
        let f2 = 9
        Task {
            print("will calculate")
            self.theAnswer = await calculateTheAnswer(f1, f2)
            print("did calculate")
        }
        …
    }

    …
}

I compiled this using Xcode 26.4 in Swift 6 mode. It works because the closure passed to the Task initialiser inherits main actor isolation, so it can access self.theAnswer. But it’s an async function, so it can call calculateTheAnswer(…). And calculateTheAnswer(…) is annotated with @concurrent, so it runs off the main actor.

When I run this (macOS 26.4.1) it prints will calculate and then, 30 seconds later, did calculate, but the app remains responsive throughout.

Now, a key component of this design is that the inputs and outputs to calculateTheAnswer(…) (f1, f2, and self.theAnswer) are sendable. Which brings us back to where we started with this thread (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Sending 'geoRegion' risks causing data races
 
 
Q