this type is obviously not Sendable:
public final
class NonSendable
{
var x:Int = 0
init()
{
}
}
but i can still pass instances of it to async functions from inside an actor-isolated method, if the instance is a local variable and not shared by the actor.
func doSomethingAsyncWithNonSendable(_ instance:NonSendable) async
{
}
actor A
{
init()
{
}
func test() async
{
let instance:NonSendable = .init()
await doSomethingAsyncWithNonSendable(instance)
}
}
but if i am more explicit about the non-sendability of NonSendable:
@available(*, unavailable)
extension NonSendable:Sendable
{
}
all of a sudden, this produces a compiler warning:
func test() async
{
let instance:NonSendable = .init()
await doSomethingAsyncWithNonSendable(instance)
}
warning: non-sendable type 'NonSendable' exiting actor-isolated context
in call to non-isolated global function 'doSomethingAsyncWithNonSendable'
cannot cross actor boundary
await doSomethingAsyncWithNonSendable(instance)
^
is this a bug? or does @available(*, unavailable) actually mean something different from saying the type is merely not Sendable?
2 Likes
tgoyne
(Thomas Goyne)
2
See SE-0337. Because most modules have not yet been annotated for sendability, the current default sendability checking mode only produces warnings for things which have been explicitly annotated.
-strict-concurrency=complete switches to the Swift 6 mode where anything not marked as Sendable is considered non-Sendable.
1 Like
beccadax
(Becca Royal-Gordon)
3
The extension marks the type as âexplicitly non-Sendableâ, so the compiler is more aggressive about telling you about it not being Sendable. Basically, it distinguishes between âweâve looked at this type and itâs definitely not Sendableâ and âwe havenât looked at this type so we donât know if itâs Sendableâ. In a 100% Swift 6 world, that difference wonât really matter anymore, but in a world full of Swift 5 code, it still does.
(Hmm, @nonSendable would make a good peer macroâŚ)
4 Likes
okay, then assuming the compiler is right, then what is the problem with:
actor A
{
init()
{
}
func test() async
{
let instance:NonSendable = .init()
await doSomethingAsyncWithNonSendable(instance)
}
}
then? where is the race condition? the instance isn't shared by the actor, it is a local variable.
jrose
(Jordan Rose)
5
You donât know (with only local reasoning) whether constructing the NonInstance has escaped it somewhere, such as by setting up a repeating timer.
1 Like
why is it that i can do this within a nonisolated actor method, and call it from an isolated method, but i cannot inline the nonisolated method into the isolated method?
1 Like
xwu
(Xiaodi Wu)
7
In settling upon a syntax for noncopyable types, it was contemplated that it should be generalizable to ~Sendable on the primary type declaration to suppress a presumption to the contrary. If we can do that, we shouldnât need a macro to generate an unavailable extension in order to accomplish this.
2 Likes
one of the benefits of @available(*, unavailable) is i can add an explanation to the message field that will be printed by the compiler:
@available(*, unavailable, message: "sessions have reference semantics")
extension Mongo.Session:Sendable
{
}
xwu
(Xiaodi Wu)
9
Good point. However, is this message displayed at the point when a concurrency warning is displayed? Even if it were displayed, is it actionable information? If a type isnât Sendable, the reason for that isnât something that an end user can do anything about.
(Incidentally, types with reference semantics can nonetheless be Sendable, so that canât be actually the full story.)
when i get a âSendableâ warning, my first thought is usually not âthere is something wrong with this codeâ, my first thought is usually âwhoever wrote this library (which is sometimes me) forgot to annotate this type with Sendableâ. so it saves me time if i am told the lack of Sendable is intentional and not just a missing annotation.
but thatâs all moot because (although i swear iâve seen the message get printed before) i could not get it to show up on 5.8 when compiling a quick experiment that misuses the type. but maybe it should?
a MongoDB session is stateful, on both the client and server sides. even if it were made threadsafe on the client side, it would still be incorrect usage.
i thought about this some more and i think i finally understand why we canât inline non-isolated stuff into isolated contexts, because when we declare a reference in an isolated context, it has the right to escape its original concurrency domain via assignment to shared actor state.
extension MyActor
{
func isolated()
{
let session:Mongo.Session = .init()
self.sessions.append(session)
}
}
and the right to escape to the shared actor state is mutually exclusive with the right to escape to some external async task, because when the async call suspends, the actor doesnât wait for it to return, it starts executing something else in its queue, and now session exists in two different concurrency timelines.
but if we declare a reference in a non-isolated context, it doesnât have the right to escape to self, so it is fine to pass it to another async function.
extension MyActor
{
nonisolated
func notIsolated()
{
let session:Mongo.Session = .init()
self.sessions.append(session)
// ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ not allowed
}
}
i wonder if what we need is some kind of nonisolated do, like:
extension MyActor
{
func isolated() async throws
{
nonisolated do
{
let session:Mongo.Session = .init()
try await session.doSomething()
}
}
}