I was trying to write a light-weight inline function that runs on the global executor. It's possible, but requires a full function signature. Is that a bug, a limitation, or something else? I see no way the function should be interpreted as synchronous, but I might be missing something.
await { @concurrent () async -> Void in
print("i am on the global executor")
}()
// error: Cannot use @concurrent on non-async closure
await { @concurrent in
print("i am not async for some reason?")
}()
Now, of course, I find it irresistible to not mention my long-standing desire for await blocks. I know that async let can get you close, but running one little bit of code off the current actor is just a common requirement that I can't seem to let it go. Anyone else like this?
let result = await {
// on global
nonisolatedSyncWork()
}
Edit: perhaps async let is just a better option of the first form?
As far as I’m aware, we currently don’t support async inference in that way. You either have to explicitly specify it as async or include an await inside the closure. But I think that would be a neat improvement worth considering, at least, for @concurrent.
I started writing out a long-winded explanation, but it's maybe more instructive/efficient to just read the PR description that changed this behavior. IIUC, basically the inference rule here was changed to reduce the likelihood of future problems if/when @concurrent is allowed on synchronous functions.
Given the above, that @concurrent does not implyasync, then I think it makes sense that the closure is not inferred to be async, since it does no asynchronous work. E.g. if you were to remove the annotation entirely, we'd expect the closure to be synchronous, and produce a warning like "no async operations in await expression".
IIUC that's not equivalent to your original code because the RHS of the async let runs concurrently with the caller. You'd have to await the async binding immediately to preserve the semantics you're going for I think (though maybe you just left that out for brevity). Also, it might be a bit less efficient to use async let if you don't actually need concurrent execution (i.e. the caller is just going to immediately await the result), because it has to make a child task and run it (though that's mostly speculation on my part).
Yeah absolutely. I think the async let-await value dance can work just fine for stuff that produces a result. And since that is often the case it can be a pretty nice tool.
Ah, right of course. I do not believe that @concurrent synchronous functions make sense, but I also understand why closing the door to them would be undesirable. Darn.
To push on this point a little bit – why not? Assuming you think actor-isolated synchronous functions make sense, wouldn't these be a fairly natural analog?
The core issue is that concurrent functions are just less-useful than nonisolated synchronous functions. For an asynchronous function, it makes sense because the await is already required. But for a synchronous function, I do not think the author of the function should be able to artificially disallow synchronous execution on behalf of all clients.
Now, I might hear another argument in favor of them that sways me. But I'm pretty sure that callers should be in control here.
(which itself is another good argument for ergonomic means of shifting work to the global executor)
Interesting, thanks for sharing that. I guess I would assume that it would still be possible to call such functions synchronously. Presumably if it's statically known that you're in a "matching" execution context, it would be allowed (same way things work with global actor isolation). I agree the utility may be rather niche, but it still seems like there's some potential value in being able to explicitly codify "function that does not run on an actor and does not suspend". Admittedly, I might just be over-weighting how it would nicely "fill in the table" of possible variations here though. At any rate... thoughts to save for a hypothetical future evolution proposal!
Ah interesting. So you are making a distinction from a nonisolated synchronous function, in that it would be impossible to run the function on an actor. Is this purely theoretical or do you have a use case for something like this in mind?
Yes, this is something you can't express today, which I think in some (again, maybe niche) cases you may want to do.
Mostly theoretical, but I think you can come up with some not-overly-contrived situations where it might be desirable. E.g. say you're processing a large image or something in parallel. Each "chunk" of work is synchronous, but you want to make sure they all definitely run on the concurrent/default executor. Something like the concurrentPerform dispatch API I guess is where my mind goes.
Ok so here's my question. Why should this function's author be in the position to decide that running synchronously on some actor should be impossible? Does it not make more sense to give the callers the option and allow them to decide to shift the work off to the global executor if they'd like?
If this is true and a strong guarantee of no actor is actually impossible, I think if anything this weakens the argument for a synchronous @concurrent functions even further. Exactly what problem could the author of such of a function be solving, other than making a presumption of the intended needs of callers?
If you want to have absolute certainty that a function executes on a specific executor, I think an actor with a custom executor is still the best approach.
This got me thinking, as a possible future direction, we could introduce something like @concurrent(SpecificTaskExecutor), which would be orthogonal to the @isolated attribute if we ever decide to introduce something like @isolated(SpecificActor).
Hmmm interesting. I'm certainly not opposed! I'm just not sure I understand the specific kinds of problems that this would help solve. Also, can this be statically checked?