Swift Concurrency Roadmap

That's good to hear. If there is some special magic happening to make actors perform better than what libdispatch queues are doing, this is certainly something that needs to be explained. Because as currently stated, actors do not seem like the best of idea.

1 Like

Irrespective of any magic, an "actor" only needs some form of mutually exclusive scheduling mechanism for its executor. Dispatch queues are but one possibility, but threads or event loops could work just as well.

2 Likes

How does that work? How does the runtime know wheter an actor should dispatch to its private queue or do differently?

2 Likes

You can override Actor.execute function (works in progress over Actor & Actor Isolation thread). tl;dr, it's a customization point for customizing how the actor would execute a (partial) task.

4 Likes

And let me guess, by default it will dispatch to its private queue, right? Which most developers will do, not understanding the consequences. I'm sorry if I sound very dismissive, I think it's great to improve concurrency support in the language, I'm just trying to make sure this will not turn into "The Return of the libdispatch". As I said, I'm afraid this has the long-running potential to be misused and do more harm than good, I certainly do not want to see that happen.

We can continue to discuss the actor execution system if you want on that thread, especially that I just asked this exact question and got answered:

Though if you're criticizing the roadmap at large (nothing wrong with that!) then we can probably stay here.

The overall shape of what's being proposed here is not that dissimilar to the Swift Concurrency Manifesto posted three years ago. That Swift would pursue async/await with actors should not come as a surprise, and without the level of detail provided by these proposals I'm not sure we'd be able to have a meaningful discussion.

These are very broad concerns lacking any specifies to ground them. An ownership model can certainly help in some cases (a uniquely-referenced entity is safe to pass from one actor to the next), but if your claim is that we need to both introduce a full ownership model and move all code over to it to enable concurrency, you're going to need to make a strong case that such a model exists and can be widely deployed for Swift.

As for the memory model, we are intentionally attacking a level above the memory model with actors. The lower-level memory model can come later, for use by experts who want to tune specific things lower in the stack. It's almost surely not the right level to focus on for the vast majority of Swift developers.

... because you can only communicate with other actors via async functions, which can be suspended, so you always make progress.

Doug

16 Likes

Which means an actor's functions can be interleaved mid-execution, is that correct? If actor A calls out to actor B and suspends, this allows another function to be executed in actor A before actor B replies and resumes execution in actor A.

Yes.

2 Likes

This makes sense to me. I think what would be helpful at this point, though, is some more detail on what considerations have already been accounted for that gives you confidence that such lower-level facilities can come later without feeling unduly bolted on or being unintentionally hampered by decisions today.

An approach like this was taken with the ownership model, where the law of exclusivity was landed first before ABI stability, but the feature was placed in the context of how it makes possible later additions.

In the same way that the direction of these current proposals isn't a surprise because they're making concrete many of the ideas of a previous manifesto, I think it'd certainly be possible to devote some discussion on how the design dovetails with the ideas explored in related areas (for example, the ownership manifesto) without necessarily committing to anything in those areas. It'd be more acknowledging that this particular design makes space for, or at least doesn't intentionally stomp over, related long-anticipated plans and reasonably foreseeable use cases, even if it doesn't take steps towards realizing them presently.

9 Likes

I’m having a little difficulty with this: is it correct that types are marked actor-local, rather than variables/instances?

That is correct for the current shape of the actor proposal, and that is how the "can't deadlock" is achieved.

In comparison with other actor runtimes, this is an "actors are reentrant by default" rather than the default taken by Orleans[1] which is "actors are NOT reentrant by default, but can be flipped to be so by annotating them.

We are taking a step into a more permissive direction than other actor runtimes here in the design.

In Akka style one has both the "dont receive any other message until this returns" as well as "do receive other messages until this returns and continue then". We built those in the library and they surface e.g. in akka persistence [2] where there is persist(...) { ... } (no other message can interleave the actor's execution between the persist call and the callback it offers) and persistAsync(...) { ... } the actor can receive other messages (is reentrant) while it waits for the persistAsync to complete before running that callback. Swift's actors today, offer the second "don't block actor from processing other things" semantics only.

In reality I do think we'll need the ability to uninterrupted/atomically { ... } or something like that... but this has not been designed yet.

Alternatively, a pretty "actor way to think about it" is to spawn more child actors for every "linear" execution one needs, so that's also a world we could end up in that would not be too weird to be honest.

[1] https://dotnet.github.io/orleans/docs/grains/reentrancy.html?q=reentrant
[2] https://doc.akka.io/docs/akka/current/persistence.html

5 Likes

That's the current idea, yes. The enforcement is static via the type system, so values of actor-local type cannot be shared across actor boundaries, including by being stored in memory that can be accessed by multiple actors, such as a property of a non-actor-local class. And you cannot "escape" a value of actor-local type into a type that's not actor-local; for example, you would not be able to convert it to Any (but you could convert it to actorlocal Any). Whether this will be acceptable for arbitrary classes is something we're going to have to figure out; there are other ways to do it.

3 Likes

im still reading but does full actor isolation mean that actors can crash/die and a swift program will keep going with other actors? if so can actors be spawned to replace dead ones?

The intent of the full actor isolation is to define away data races by preventing code from concurrently executing conflicting accesses to the same non-atomic memory. That has always been the goal. To the extent that we achieve it, we will be able to assume a single-threaded model for non-atomic memory; to the extent that we fail to achieve it, we will continue to rely on the undefined behavior of such accesses, and so again we will be able to assume a single-threaded model for non-atomic memory. In no case do we have any intention of defining semantics for concurrent conflicting accesses to the same non-atomic memory.

That then leaves us with two basic questions:

  • When is memory "non-atomic"?
  • When are two locations in memory "the same"?

Both of these have had answers for a while now, although maybe we haven't communicated them effectively:

  • All memory is non-atomic except as accessed through explicitly atomic facilities. Swift offers only extremely modest atomic facilities right now, but we expect to fill these out in time. Those general atomic facilities will almost certainly be declaration-driven. The restrictions that we intend to impose as part of data isolation will also be declaration-driven. So we expect that it will be straightforward to identify a general principle that allows atomic declarations to be safely accessed from multiple actors/threads when other declarations can't be.

  • Two locations in memory are generally "the same" in this sense if they are part of the same containing object, which is to say, a local variable, a global or static variable, or a class property. Different properties of the same struct are generally "the same memory" for the purposes of judging this. Note that this is, not coincidentally, strongly analogous to the rule used by exclusivity.

Note in particular that our actor isolation model does not depend on deep-copying objects and preventing any memory from being shared between actors. We intend to allow memory to be shared as long as there is something ensuring that it is used safely:

  • it could be statically a unique reference
  • it could be immutable if we don't know dynamically that it's a unique reference
  • it could have only fields that are somehow safe:
    • perhaps they're restricted to only be accessed by a particular actor
    • perhaps they're atomic
    • perhaps they're explicitly unsafe

So that's why we don't think anything we're planning to do will prevent us from implementing a more complete atomics library.

13 Likes

No, that's not how we're using that term. It's an interesting future direction, but we think it would far too to try to impose on the standard Swift ecosystem — it would need to be opt-in in some way. So it's in our mind's eye, but we're not looking to design it now.

4 Likes

I'd like to second this comment. So excited for this!

3 Likes

Awesome, I am very excited to see this coming together after so much work over the years on things like exclusivity and the other fundamentals that have gone into this. I'm also happy to see that it is generally aligned with the rough concurrency manifesto outlined earlier. The approach is better in several specific ways than that outline, e.g. the elimination of deadlock is very interesting and could be great (but also needs to be carefully considered, because there are tradeoffs).

Also fantastic, I think this is a really great programming model which will lift the programming experience in Swift and provide a strong conceptual foundation for concurrent programming.

As I related to multiple core team members in previous communication, I think that splitting this into two phases like this (and the proposed approach for the second stage) is extremely concerning for several reasons:

  1. A major point of introducing actors is to get rid of shared mutable state -- and the corresponding bugs that go with them. This is the key to providing a safe concurrent programming model, and one of the major failures of actor implementations like Akka. Stage #1 doesn't achieve this.

  2. Actors will be adopted by the community very rapidly, and a lot of code will be written against "stage 1" of the design. Doing a hard source break in a subsequent release of the language is going to fracture the community, and cause unnecessary problems for adoption.

  3. The proposed "Stage 2" solution to memory isolation (actorlocal, mutableIfUnique, et al) doesn't solve the general use case (it covers a few specific subcases) which means that "stage 2" will be less expressive than "Stage 1". This means that it may be very difficult to adopt even for people who are willing to rewrite their code.

  4. The proposed approaches for achieving memory isolation are type system intensive (making the language much more complex), will be difficult to explain to non-super-expert Swift programmers, and will have some fairly concerning semantics implications that may make them not adoptable in general. We should provide a simple model that is easy to explain.

  5. The description above makes it sound like you're not interested in the extremely important case data structures that use fine grain locking and lock free structures internally. While I agree that it is good to push programmers (by default) to just use actors to protect shared mutable state, I also think it is important that we allow expert programmers to build powerful libraries that compose with actors correctly.

I believe that there is a simple solution here, which I have been discussing with JohnMC. The description above does a good job of summarizing the two issues, which are cleanly separable:

I will try to find some time this weekend to write the reference type issue up, it seems completely additive to the model proposed here, and will resolve the concerns above. If that is well received, we can talk about globals.

I will also try to review the initial proposal drafts in detail to provide a round of feedback when I can. It is good to see these coming together, but there are a lot of details that will require a significant amount of iteration. While it is super useful to see a draft of these all at once (to understand how they fit together) it will be challenging to keep all the moving parts in my head as the design changes over time. I hope we can serialize some of the formal reviews bottom-up (e.g. starting with async functions).

-Chris

41 Likes

Awesome! Very exciting indeed! I have a few questions

Question 1:
Can we use await in test methods? How would timeout works?

Question 2:
(addressing devs working at Apple)? Are there any plans to easily create A Combine Publisher by passing in a async function? I think this is possible in JavaScript land, creating Promise (part of JS itself, not RxJS I believe) by passing in and async method.

Question 3:
How will async/await live alongside FRP frameworks such as Combine and how should I reason when choosing between these two rather different tools/solutions when designing asynchronous parts of my code?

If the answer to Q1 and Q2 is YES we can easily use async methods with XCTest, then it feels like it makes sense to prefer design my code with easy to test async methods and then “as late as possible” turn them into reactive streams when I see need for Merge/Combine/Map etc. What are your thoughts in the matter?

Question 4:
(Relating to Q3 maybe, also relevant for Apple employees) do you plan to make use of async/await and Actors in SwiftUI?

5 Likes

I get a similar felling. Introducing a feature and then adding restriction to it short while thereafter seems like a recipe for discontent.

Perhaps the two stages should happen in parallel. For instance, Swift 6 and 5.x language modes could be introduced simultaneously with 6 coming with full actor isolation while 5.x comes with the same thing minus breaking changes (and is therefore not fully actor-isolated). This way projects can migrate to full isolation at their own pace while still being able to interoperate with other modules using actors. Actors support in 5.x is then be seen as a compatibility compromise for using a Swift 6 feature in an existing code base and would produce warnings (perhaps via a flag) for things that'd be an error in Swift 6. This way you can migrate code progressively while keeping it in a working and testable state.

If some code can't be migrated to 6 because of missing things, it can continue using 5.x for a while. Then when 6.1 make things better, its improvements also become available as 5.x.y for backward compatibility and the progressive migration process can continue. This feedback loop can continue for a while until we're confident all the important use cases have a good migration path.

Terms of Service

Privacy Policy

Cookie Policy