Well, if I'm understanding right, it is just MainActor
-default isolation. And I think that would be ideal for exactly this network decode situation and for many other of Apple's own SDKs.
"Many" is interesting, but it is not all. I prefer to push the design team into solving difficult problems. Staging language and SDK improvements is excellent, but skipping a few foreseeable steps is not wrong.
This is a really interesting point. I want to think more on it.
But the reason I bought up warnings earlier is I'm not sure this is actually what the feedback is about. Many, many people adopt Swift concurrency features incorrectly believing it works certain ways because they have the compiler feedback disabled. They progressively introduce more and more into their code base.
And then, only after they've gotten quite far, they turn on the warnings and realize they have a mess on their hands. At this point, I think it is quite natural to come to the conclusion it is the language that's the problem.
I think that situation is far worse than starting with a purely-GCD-based (for example) system.
I cannot speak for other programmers, as I don't know if they use Swift Concurrency the wrong way or turn on diagnostic too late. I can only speak for myself. And the thing is, Swift Concurrency safety in its current form does not allow me to write a single-threaded module that does not use Swift Concurrency at all. And when I call a spade a spade (not even call BTW, but politely wave my hat and ask to enlighten me whether we have a spade already), my post is being flagged as inappropriate. So yes, end users like me is the problem that needs to be fixed.
Can you expand a bit here I do not quite understand. Is your goal to use the Swift 6 language mode without running into any errors and also not using any Swift concurrency features?
There are constructive and destructive ways to communicate. It's possible to deliver good and useful information in an unacceptable way. And doing that completely undermines your message. I have flagged a post exactly one time on this forum, and it was in this thread. There is no room in the community, this forum, online or anywhere else, for that kind of behavior.
And it's doubly-challenging, because the moderators of this forum are also involved in the project itself. In my opinion they endure more abuse than they should, because they are trying not to appear defensive. We should not be putting them in that position. I am extremely sympathetic to frustrations around concurrency specifically, but we just don't talk that way here.
Now, all that said. I think the vision document went pretty far to acknowledge real problems, including very specifically that it can be hard to make single-threaded programs. How do you feel about what has been presented?
I can sympathise with the sentiment that Swift 6 concurrency could have an easier on-ramp for newcomers to the language. (Although, FWIW, actually using the Swift 6 language mode turns out to be much easier than the Swift 5's strict concurrency mode.)
However, I'm not entirely convinced from this document that a new language mode will serve to simplify the language. I'm concerned it may do the opposite.
As @gwendal.roue points out, it won't be long until you need to dip into this 'full fat' Swift toolbox, and so now instead of a single language, beginners need to learn two language variants.
If a new language mode/variant is an absolute must, then I feel this should be annotated at the file level. Needing to dig into a project's build settings to determine the 'language mode' of a file seems like a downgrade in terms of DX, and I can't imagine it will do any favours for developers using AI coding agents to help them, either.
As a iOS dev for 14 years, I feel a little sad at this statement, but I agree with it, and I really like how Swift handles concurrency, including the strict checking.
In fact, it's also helping us catch a lot of potential issues: I say "potential" because through trial and error, over the years, we ironed out many low level data race problems, and we still have more, but the crashes are rare enough that it's hard justify putting time and effort into fixing them. But I'm sure that, eventually, when we are able to enable strict concurrency in more parts of our code bases, things will further improve, and we will prevent potential future issue, which is the whole point.
We don't have many classes, even mutable classes: when we need shared mutability, we generally use different techniques, and in many of those cases, enabling strict concurrency very clearly showed us several footguns in plain sight.
The fact that, in practice, right now, many iOS devs don't have many actual low level data race crashes is not an argument against having this feature in an ambitious language like Swift: the argument "I don't have this issue, leave me alone" is fallacious. I do appreciate, though, the fact that the LSG has acknowledged that there's a progressive disclosure issue with strict concurrency checking: progressive disclosure is and must be one of the key tenets of Swift, and I agree that a better job could have been done here, but given limited time and resources, I think that overall the core team prioritized the right things.
I'll provide some feedback about the vision document, but I'd like to summarize first our experience as a large team of iOS devs working on 2 large, heavily modularized (many packages, with many modules each) app projects.
Our experience
In our case, the main issues have been documentation and migration.
I (and others in my team) followed the proposals, watched the WWDC talks, and tried to internalize all available documentation while exploring these concepts, but it wasn't enough. To actually understand how to leverage these concepts, and apply them to our codebases, we needed to poke around, experiment, do "workshops" in which we tried out concurrent code and:
- observed the actual behavior with logs;
- looked at compiler warnings and errors, and tried to interpret them.
After a few weeks of poking around, we concluded that we had to do 2 key things:
- make almost everything
Sendable
- isolate lots of things to the
MainActor
.
In retrospect, these are kind of obvious, but it's a been a laborious process, that's still ongoing (in fact, the progress is still relatively low, due to many other priorities).
Point 1.
was related to the fact that, once you get that Sendable
means "not causing concurrency issues", you definitely want most things to conform to it. Our specific issue is that, because our projects are heavily modularized, and include lots of Swift targets that depend on each other (following a certain architecture, of course), we have several public
and package
declarations that needed an explicit mention of Sendable
. Also we use a lot of higher-order functions, and most of not all of the function arguments needed @Sendable
. It would have been great if we had some per-file assumeSendable
declaration that implicitly added Sendable
or @Sendable
where needed.
Point 2.
is also kind of obvious, but it's the "the fish don't know what water is" problem. At a certain point, we realized that we needed to mark all top-level declarations with @MainActor
, but they were already referred from the "main actor" in practice, even before structured concurrency, it's just that strict concurrency required to make this explicit, which to us is actually a good thing! It makes things clearer, and consequently safer. As with the Sendable
thing, a possible added convenience would be to be able to declare that all top level code is isolated to the MainActor
on a per-file basis, with some assumeMainActor
declaration.
When you think about it, these are not that different from the async
and await
keywords, when you actually understand them: if a function is async
, it's saying "this could be suspended, to free up resources", and await
means "suspend the current function, to free up resources, until something else is done, then come back". Considering this, a lot of functions should be already async
, and the fact that many that should be are not is really a legacy code issue.
In fact, I think that if Swift development started today, Sendable
, @MainActor
and async
would likely be the default (requiring then an explicit opt-out), and the fact that we need to add these declarations in many places of our code bases today is caused by the need to maintain retro-compatibility with existing code. I know that it's a simplistic overview, but when you really look into it, it seems to me that almost everything should be Sendable
, many things should be isolated to MainActor
, and most functions that deal with control flow should be async
.
In practice, though, we had to implement some tricks to avoid excessive changes to the code bases, that would have required too much time and effort. The need to use async
functions in non-async contexts, that should have been async
in the first place but would be too laborious to migrate, forced us to introduce a Mutex
here and there, that we implemented like this:
@dynamicMemberLookup
private struct Mutex<Value>: @unchecked Sendable {
private let valueBox: Box<Value>
private let lockBox = Box(os_unfair_lock_s())
init(_ value: Value) {
valueBox = Box(value)
}
var value: Value {
lock()
defer { unlock() }
return valueBox.value
}
subscript<Property>(dynamicMember kp: KeyPath<Value, Property>) -> Property {
value[keyPath: kp]
}
func withLock<Output>(transform: (inout Value) -> Output) -> Output {
lock()
defer { unlock() }
return transform(&valueBox.value)
}
private func lock() {
os_unfair_lock_lock(&lockBox.value)
}
private func unlock() {
os_unfair_lock_unlock(&lockBox.value)
}
private final class Box<T> {
var value: T
init(_ value: T) {
self.value = value
}
}
}
we're actually not 100% sure that this is sound, and it is shown as private
here because we explicitly don't want to publish it in a shared library, in order to discourage its usage and encourage a proper migration to async
, but in some cases it was required, and it would have been nice if the standard library already provided something like this (but actually sound) in order to help with the migration to async
. It could be called UnsafeMutex
, to highlight the fact that this shouldn't be used in general, but it's a convenient escape hatch for cases where there's no feasible alternative.
Thoughts on the vision document
Given our experience, it seems to me that defaulting to MainActor
for top-level declarations, alongside inheriting the current isolation automatically when calling non-isolated async functions (a.k.a. same behavior for non-isolated sync and async functions) will help solving most of the issues people currently have, and had to force themselves through in the process of migrating to strict concurrency checking.
I think the vision document tackles these problems successfully. But I also really appreciate the statement
we donât want these usability wins to create pervasive regressions or to make it frustratingly difficult to achieve a high level of performance
I do appreciate the resilience against some extremely negative (completely unjustified, other than offensive) remarks that had floated around the forums in the last few months about strict concurrency checking.
The problem is not that the adopted solution is "overly complex" (that's also completely wrong to me): the problem is that progressive disclosure was, in part, neglected.
But this might highlight a potential issue with the vision document. For example, these statements
we want to maintain a smooth path for experienced programmers to opt in to concurrency and maintain the safety of complete data-race checking
When the programmer is ready to embrace concurrency to get better performance, they can explicitly offload work from the main actor to the cooperative thread pool
basically assume that "experienced programmers" will embrace and understand why and how strict concurrency checking works. But some negative feedback has come precisely from experienced programmers that are convinced that they can solve shared mutable state access problems only thanks to their programming skills, and even the very simple and basic need to mark a mutable class with @unchecked Sendable
is too much.
I understand the fundamental good will behind the vision document, and the focus on progressive disclosure, but I think it would be interesting to also add some remark that reaffirms that these language-level features exist for a reason, that is, enable local reasoning for complex issues, to help programmers write correct code, even if they feel like they can already do so by sheer brain power. As an iOS dev, I can safely say that parallel access to shared mutable state is 100% also an issue in app development.
Another small issue that I see, related to both progressive disclosure and documentation, is exemplified by this remark:
if a function or type is not annotated or inferred to be isolated, it is treated as non-isolated, meaning it can be used concurrently
The fact that "non-isolated" means that it can be used concurrently is not super clear, TBH. If the default must eventually be "non-concurrent", then it is "non-non-isolated", that is "isolated", but to me the very concept of "isolated" should be progressively disclosed at a later stage: the language was designed to assume a different default, that is, "non-isolated", thus, concurrent, and the fact that flipping the default introduces a permanent language dialect is tackled in the document, but if I want to introduce concurrency in a default non-concurrent module, why not introduce a concurrent
keyword, to use instead of nonisolated
?
Also, as already mentioned above, in terms of readability and clarity, it would be interesting if the declaration for the flipped default was added at file level: adding it a module level is convenient, but less clear overall, and it would require creating a separate module if, for example, defaulting to MainActor
was required only for a group of files in an existing module. The two don't exclude each other, of course: we could have the default flipped at module level and/or at file level, and if we add the flipped default declaration on top of a file in a module that has already a flipped default, the compiler could just emit a warning about the fact that file declaration will be ignored.
As I mentioned, our app projects are heavily modularized, and many Swift targets exist mainly for the purpose of code organization: for those modules we would use the flipped default, but many other modules, that contain shared tooling instead, would benefit from the possibility of file-level flipped default declaration.
There's a remark in the document about providing some limited tool to wait on asynchronous work to complete. This might look similar to the need of a Mutex
type in the standard library, but I'm not sure that's the case. Arbitrarily waiting on an async work (without suspending) makes no sense, because it's effectively wasting some computing power that could be used elsewhere. I think this point is not generally understood enough: it's anecdotal, but in the conversations I had about this "need" to synchronously wait for asynchronous work, the developers I talked to didn't actually realize that they were tying up system resources.
I would instead try to document and explain better what await
means.
The remark is
Other concurrency libraries like Dispatch provide a limited tool set to wait on asynchronous work, such as DispatchQueue.asyncAndWait. These tools come with serious tradeoffs, including tying up limited system resources and introducing the possibility for deadlocks, but they provide critical functionality that is sometimes necessary in a project.
I'd like to see examples of this "critical functionality that is sometimes necessary in a project". One might think that having something like await!
, to block until the asynchronous work is done, is going to be similar to try!
or the force unwrapping of an Optional
, but those are very different cases, related to the fact that sometimes the compiler doesn't have enough information to infer something that the programmer could instead easily infer.
I would instead document better the patterns that leverage proper usage of await
, and the (good!) architectural changes that they encourage.
It's certainly how things used to work - every request was scoped to a particular event loop and you didn't really have to worry about threading issues. That assumption completely breaks with Swift Concurrency for now since you have no idea where your task will be executed and one of the reasons why migrating Vapor was so painful - we had a lot of assumptions about where things would run that no longer hold up.
This should change with custom task executors and we can scope tasks and child tasks to a particular event loop, but that requires more work
Yes.
I am glad that the problem was acknowledged and that there is a vision of the need for its solution. This was presented in a very thoughtful manner and is very much appreciated.
My post in another thread on this forum was flagged and hidden some time ago when I quite politely asked if we have exactly the same kind of problems that are discussed in this thread. That's why I reacted too emotionally when you shifted the blame to end users. My apologies for that.
Ok that makes sense, and I'm also pretty optimistic here!
I don't have the context, but I'm sorry you had that experience. I don't even think the bar has to be as high as requiring politeness. I think critical feedback is wonderfully useful and very welcome.
I do want to be extremely clear though that I was trying to highlight how problematic is it that the language's default is to accept incorrect concurrency usage without diagnostics. This has lasting impacts on the mental model for users. Unfortunately that does color feedback sometimes and make it less useful. I guess ultimately that really is a form of indirect feedback on the understandability of the system, which is totally valid too.
So, I do not accept your apology because if I had being blaming users you should get upset! It's my fault for not being clearer. The language is a tool for its users, not the other way around.
Your post was flagged as inappropriate : the community feels it is offensive, abusive, to be hateful conduct or a violation of our community guidelines.
Wow.
If anyone is interested - the content is maintained in quotes above.
I wonder how much of the objection is cognitive dissonance from people invested in the current state. It's hard to recognise that something which has involved an extraordinary level of work might not be good for 'regular devs'
I certainly presented a very negative view of where swift 6 concurrency is today. And I believe it to be true.
You can police my tone - or engage with the argument...
Forgive me for oversimplifying, but from my understanding the basic proposition is to make @MainActor
everything to be the default, rather than nonisolated
as it is now.
To me this makes a tremendous amount of sense and honestly I'm not sure it needs a separate language mode for that. I would love to see this happen.
The assumption is that library code would/should have a different standard to uphold. As a library author I'm very sympathetic to that, but I'm still not sure a separate language mode is the best approach there. Wouldn't it still be better to just have @MainActor
as a default everywhere (everywhere!) and language authors will need to figure out where to explicitly put (hypothetical) @NonIsolated
and nonisolated
annotations? Wouldn't that fit in the spirit with progressive disclosure more and be a better default for all new users, including those dipping their toes into authoring libraries?
I'm probably missing a lot here, esp. surrounding protocol conformances. To be honest I'm not even sure I've got a good hold on that in Swift 6 mode today still. But to me keeping this sane, progressive-disclosure-supporting, default everywhere would be a more interesting direction.
The amount of @MainActor
wrangling, and Sendable
wrangling one must do (the latter mostly because things are not @MainActor
by default) is very high at the moment. The "everything is single threaded unless it's explicitly not" direction is one to lean into even more IMO.
I wanted to second the observation raised above about considering how these changes might affect the âlocalâ readability of code.
A use case I think about a lot is code review. I personally find code review of Swift Concurrency-related code to be difficult. I think part of the challenge comes from the complexity of the model (heuristics such as rbi, etc.) being a challenge to âcompile in your headâ, and part due to our preference for brevity: there is not a lot of âlocal informationâ in front of your face when jumping into a file to review.
In some cases Iâve found I donât actually understand what Swift Concurrency-related code Iâm reviewing is doing until I compile, run, and step into it in the debugger. I have also observed teams build completely reasonable-looking features based on Concurrency that actually donât work at all. It was striking to me how these features âlooked rightâ â it was only by careful dissection in the debugger and with tests that we found some fundamental misconception with them.
I think preserving âlocal reasoningâ is in some ways in conflict with a desire for brevity and pursuit of progressive disclosure, and I think itâs possible that layering on even more modes and heuristics will make things such as code review (something all of us probably do regularly!) even more challenging.
I am generally skeptical about the whole @MainActor
-all-the-things idea, but I get what it tries to achieve for app targets.
However, I personally think it may be the wrong default, and I am convinced it is the wrong default for library code (or really any code that is not a demo app or a simple script).
It sure would feel like a big departure for a "general purpose" programming language to force every type to run its stuff on the main thread unless opted out.
Honestly, the entire premise of "Introduce parallelism to improve performance" does not quite sit well with me. It's not that I think it is wrong in terms of a progressive disclosure journey, but deriving from that that types should generally be "main-thread-only" (because multi-threading is such a rare special thing for performance optimization) is clearly problematic (I hope we can agree on this?).
In my opinion, most code (ie: the default) should NOT force you to run in any specific isolation, it should work in all contexts. And I think once we get the proposed Inherit isolation by default for async functions
changes in, that would simply mean: less @MainActor
, embrace non-sendables, have RBI and sending
do their thing, and don't use mutable shared state without locks/actors -> and life will be good.
I have to say, I find this pretty compelling. The more I think about MainActor-by-default, the less I like it.
The UI world (AppKit, UIKit, SwiftUI) are fairly well served today by the frameworks internalizing the needed isolation and applying it via inference to subtypes. It's possible to write quite a lot of code without needing to use annotations.
However, managing isolation becomes pretty rough as soon as you want to use concurrency with non-Sendable types. The inheritance rules proposed will, I think, just make that problem go away.
This change will also help libraries. I just finished up migrating a library of my own from using MainActor everywhere to being usable from generic isolation via isolated parameters. It was very hard, for many reasons. But that will also, I think, just become a non-issue when the inheritance rules change.
I'm really excited about that proposal.
And combined with isolated conformances and isolated subclasses/overrides, I think the situation is going to be tremendously improved for most of the situations where MainActor-by-default would help - except for globals.
Was any thought given to applying MainActor-by-default to global variables only?
I guess the point is â if you're writing a new non-main-actor-isolated function you don't want it to explicitly jump to the main actor until you start extensively opting out of @MainActor
explicitly. If so, I agree completely.
I think your point is that each function should automatically inherit the isolation of the caller unless specified otherwise. That makes sense to me for freestanding functions (and maybe closures as well), but what about calling a class' member function? There is no reasonable way of also automatically isolating instance variables to each caller (when there could be multiple concurrently). I think that's the core of the issue.
Is it such a bad default to have default @MainActor
on classes (only?) unless @Nonisolated
is specified? For structs and enums I'm less sure and I think your suggestion makes more sense as a default.
That is the beauty of it: if it is non-sendable, there are no multiple concurrent callers, because it cannot be sent.
edited addition to this point:
I think this is one of the most underutilized (or maybe least understood/internalized) properties of non-sendables, and I suspect this is the reason why people over-reach for global actors. Non-Sendable types, in combination with inherited isolation and the sending
keyword for transferring, are what you should reach for first IMHO. Then locks/mutexes or actors, and only then global actors.
edit 2: I was referring to mutable shared state/classes. More generally, sendable structs should probably be the first weapon of choice.
If it is Sendable, you will have to manage locking or the compiler will complain.
I don't get it. Why should one reach first for a non-Sendable
type? For example, a struct
with Sendable
properties will be Sendable
automatically, but under your assumption should this be considered an issue?
Sendable
means "it's not going to cause data races", which is something always desirable. In some cases the compiler will be able to infer automatically that, yes, that type is not going to cause data races, but in other the dev will need to tell the compiler to trust them with @unchecked
: what's the issue with this?
EDIT: my bad, I didn't interpret the message right