Sendable in Foundation

I noticed in Xcode 14 betas that UserDefaults is explicitly marked non-Sendable, even though the documentation states that it's thread-safe.

I thought this might be because it's an open class, but Foundation doesn't seem to treat that as a concern when adding @unchecked Sendable, for example URLResponse is likely to be subclassed, but is still marked Sendable.

It'd be really helpful if the ObjC "sendability audit" macros contained reasons why a class is marked Sendable or not, because in some cases it can be quite subtle!

In general, I'm struggling to understand some of the decisions that have been made. For example,

  • NumberFormatter is marked Sendable, despite having mutable state, and unlike any of the other formatters
  • NSNumber is marked Sendable, unlike NSValue and despite being open and having known subclasses
  • FileHandle is marked Sendable despite having mutable state (readabilityHandler etc)
  • Operation has many methods that only make sense when used from other threads, but is not marked Sendable, possibly because it's intended to be subclassed, but then again that didn't stop URLResponse
  • OperationQueue is not marked Sendable despite the documentation implying it's thread-safe

It feels like there's some mistakes here, and some cases where subtleties are lost due to a lack of expressive power in the language(s). For example, perhaps

  • An ObjC feature to make a class non-open, and export that to swift
  • An attribute like @subclassesMayBeSendable for a class to say that it, itself, could be Sendable but wishes to allow subclassing (like the special treatment NSObject gets)
  • An attribute like @subclassesMustBeSendable, which extends the previous attribute, but also allows any instance of this class to be treated as Sendable (like URLRequest apparently needs)
8 Likes

I've just come across the same issue.

I don't think subclasses are a reason for classes not being sendable.
For example the docs state, that as of iOS 7, NumberFormatter is thread-safe. So its sendable-conformance makes sense. Its mutable state is likely protected by locks.

FileHandle on the other hand is interesting, since these very old docs list it as unsafe! However, it could be that this has changed since those docs were written.

Operation is likely not marked Sendable because it's not entirely thread safe. It has thread-safe methods and properties, but Operation is intended to be subclassed and could then have properties that are only accessible on the queue the operation is run on.

URLResponse could be marked Sendable, because its subclasses mostly live in Foundation AFAIK. So they're all known to be thread-safe.

OperationQueue on the other hand should be Sendable IMHO. Same goes for UserDefaults.

The latter two are what I find most confusing. In Xcode 13, we've modeled our application using the thread-safety docs. Thus we assumed UserDefaults to be Sendable, slapped a @preconcurrency on the import Foundation and called it a day. With Xcode 14 we suddenly get warnings here, that make no sense to me.

I've submitted FB11224364 covering UserDefaults and OperationQueue.

3 Likes

It's a problem when the subclass may not be Sendable, because instances of subclasses, statically typed as the parent, may be sent unsafely. That's why automatic/safe Sendable conformance is not available to non-final classes. @unchecked Sendable is obviously available to any type, though I feel that it probably shouldn't be available to non-final classes either.

I also find myself wondering about Swift's own String, which may be backed by an NSString. Immutable CFStrings should be Sendable, but NSString is arbitrarily subclassable, and it's not clear to me from inspection of the code that Swift's implementation guarantees that String is only ever backed by an immutable CFString (it seems to use NSCopying to duplicate the string, which any subclass would be free to implement in any way).

I think you could argue that stepping into ObjC in any way removes any safety guarantees from Swift, but the fact remains that the primary way Swift is used these days is intermixed with ObjC, so it's not a completely theoretical concern.

I'd filed FB11180190 "UserDefaults not Sendable" prior to making this thread.

Which is likely why e.g. URLResponse is marked Sendable, because you don't create new subclasses yourself and all the framework subclasses are safe.
Whereas Operation isn't marked Sendable because here, most of the subclasses live outside of Foundation, thus it cannot be decided at the base class level that they should be Sendable.

Except even the concrete subclasses of Operation, like BlockOperation aren't Sendable either, which doesn't make much sense. Those values are specifically sent across concurrency boundaries already, so surely they must be Sendable, no?

1 Like

But you are explicitly recommended to create URLResponse subclasses: Apple Developer Documentation (URLProtocol)

(And I don’t see anywhere in the docs any suggestion that URLResponse subclasses need be thread-safe)

Responses aren’t usually mutable, which is why they’d be thread safe. It would be nice if we could see the justification, one way or another, in the documentation.

But you are explicitly recommended to create URLResponse subclasses

That idea dates from a time when URLSesssion (well, at that time it was NSURLConnection) actively encouraged folks to create support for new protocols. That time has passed IMO. While creating a new protocol is still possible, the world has changed to make it much less useful:

  • Historically, Foundation would load custom protocols from plug-ins that you could install on the system, so you can add a protocol that would then become available to all NSURLConnection clients. AFAIK that didn’t survive the CFNetwork rewrite.

  • URLProtocol has not kept up with the evolution of the API. This is something I discussed in depth in the CustomHTTPProtocol sample code read me, and things have got worse since then.

  • Many requests run out of process — URLSession background sessions, WKWebView, everything on watchOS — and custom protocols don’t work in that case.

Given this reality, I wouldn’t let that one line of documentation affect you thinking on this topic too much.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

4 Likes

Not necessarily. The following code would generate an error if the closure passed to block-operation would be @Sendable:

var didRun = false
let blockOp = BlockOperation { didRun = true }
let queue = OperationQueue()
queue.addOperation(blockOp)

Here's the code with the error:

func makeBlockOp(with block: @Sendable @escaping () -> ()) -> BlockOperation {
    .init(block: block)
}

var didRun = false
let blockOp = makeBlockOp { didRun = true } // error: Mutation of captured var 'didRun' in concurrently-executing code
let queue = OperationQueue()
queue.addOperation(blockOp)

So here, not making BlockOperation conform to Sendable (which would in turn probably require its closure to be @Sendable) could likely also be due to backwards compatibility.

I'm sorry but that doesn't make much sense to me. Marking your closure @Sendable simply activates the compiler warnings for using it with mutable values. Such usage is already unsafe without the markup, it's just the the compiler doesn't know it. So it seems like it should be Sendable but rely on the compiler's warning level not to produce new warnings on current code which is technically in violation. I really hope Apple isn't resisting marking types as Sendable because the Swift compiler's concurrency warnings are still too sensitive not to break code.

The issue here IMHO is that using an @Sendable closure will not generate a warning but an error! Thus breaking existing code. Whether that should be a warning or not is probably a topic for another discussion, but in this case it was probably easier to not mark it as @Sendable to prevent breakage.

Also, my example above can be completely safe if the didRun variable is only read and mutated from within the block operation, which in again runs on a single queue.

Don't get me wrong, I totally agree with the reasoning that Operations match the idea expressed by Sendable! I just think, that they come from a world without Sendable and are thus made safe in other ways, e.g. by using rules that cannot be expressed by Sendable today. As in above's example - which is safe using the rule that no one is allowed to read or even write didRun outside of the block operation's closure.

If Sendable doesn't mean thread-safe, then I don't know what sendable means because I thought it was much more than immutable.

Ackhtually according to the docs, one of the categories to be automatically considered Sendable is Reference types that internally manage access to their state.

Since UserDefault is a reference type that is thread-safe, it should be considered Sendable meaning this Swift compiler warning is contradictory to the Sendable docs.