Progressive disclosure - 00:00:37
You don’t need to onboarding everything all at once onto Swift Concurrency. You can only onboard a single function, class or module at a time.
Actors - 00:04:12
Similar to class
, struct
and others, actors are a type. They’re named actor because: XXXX .
Actors give you synchronized and thread safe access to all properties and functions.
00:08:20 - This comes at a cost. Any access to any member function of the actor from outside will have to be marked with await
Isolation - 00:10:48:
Is something isolated / protected from read / writes that could break thread safety? Actors provide such isolation.
- Task
- Sendable
- Sending
00:00:37 things like it’s so beautiful that it does Progressive disclosure you can start with your async function and await them and Swift won’t yell at you about it then you use task groups and tasks and chall you go nuts with it you go as far as you you want use actors it’s all fine Swift loves it and then you might have turned on strict concurrency or started looking at Swift 6 and then Swift started actually yelling at you for doing the simplest stupidest things is telling you passing closure is ascending parameter risk causing data
00:01:03 races between coding the current task and concurrent execution of the closure and you’re like what okay fine or maybe you see something like this sending main actor isolated instance cell view model to non-isolated instance method load items and you’re going to stop breathing there I’m not sending that view model anywhere Swift it’s not going away non sendable type movie returned by implicitly a string it’s not implicit I awaited it on purpose I don’t don’t know what Swift is thinking but Swift 6 is making my life
00:01:33 harder and probably your lives even harder so yeah Swift 6 um concurrency did make us a bunch of promises promises as and syntax we have that and it’s pretty decent it’s pretty good if you only use that you’re fine promises structured concurrency that actually works pretty well uh and it promises compile time database protections and that is the bane of everybody’s existence that’s trying to do Swift 6 at this point so I took the liberty of breaking that part data race protections down into three main pillars they’re defined by me
XXX 00:02:07 made up about 24 hours ago or so unofficial chat GPT doesn’t even know I wish it would have said that like Donnie walls def definer of the Tre pillar but it didn’t um so we have isolation we have actors and we have send ability the three topics that I would love to talk about today I think if you get these three you will understand the compiler still hate them you still won’t be able to solve them but at least you’ll be able to understand what the compiler is trying to tell you because the compiler
00:02:37 has its own perspective on your code you might have code that runs perfectly fine and you know that it’s free of data races but the compiler reads it and it goes no that’s not fre your data races go yeah but it is no you see you could in the future if you wanted to maybe potentially run these two things at the exact same time and you’d have a data race so we need to understand how the compiler looks at our code and how we can tell the compiler when it’s wrong how we can nudge the compiler to understand us and also sometimes we need
00:03:10 to know to compil this perspective so that we can know when to give up on Swift 6 and go back to strict concurrency checks and only have warnings so actors sendable and isolation where do we even start over the past two or so months I wrote this talk starting with isolation I wrote this talk starting with sendible I wrote it starting with actors and then I realized let’s make a vent diagram it’s basically a circle like they’re so closely related but we have to start somewhere and it’s hard to talk about them in XXXXX
00:03:43 isolation I know half the crowd can’t see that but that pun was absolutely intended I decided to start with actors and that’s because actors sort of bring everything together when I wrote The Talk starting with isolation most of my examples involved actors when I want to talk about s guess what I use as examples most actors so I thought I’ll explain them first and then we sort of tack the rest on and hopefully by the end you understand all three well enough to understand what the compiler is trying to tell you so if we
00:04:12 don’t have actors so we go back a little bit how do we protect State like Christian trying to do right [Laughter] now you using that yeah did you try this have you considered that one yet right we can use locks to make sure that we gain exclusive access to resources and we can let go of the lock to let the system gain other resources you can use dispatch Q to put a bunch of work on a queue and make sure that only one thing at a time executes we can use semors to limit access to a limited number of
00:04:48 resources um and it it works right these things are not available in Swift concurrency in the same capacity but you know we have something before however it was complet up to us to make sure that we use these things correctly I mean I actually looked at Christian’s code today and he’s using the things correctly as far as I could tell but he has a data race only when he presses enter in Swift UI can you imagine he can type anything he wants when he presses enter boom and it’s notoriously hard to debug I love
00:05:17 that just this example happened today and it’s not hard to write unsafe code right we can write code like this an uploader with a private variable upload task we can start uploads you pen to a list what’s wrong with this this will crash probably maybe I need to add a little more uploads but this could crash the reason is when I start an upload I append to an array when I do that twice at the same time I’m appending to that array concurrently which means I’m modifying memory in one address from two places and That’s a
00:05:50 classic definition of a data race so this would crash if I’m lucky I get this pointing to a line of code somewhere in my uploader if I’m UNL unlucky I’ll get a crash log saying CF TCH pointer uh something the table view I’ve seen dates I’ve seen come across I’m like I’m not using that anywhere so what’s actually wrong it’s almost impossible to figure out unless you start actively hunting for your data races so writing that safe code is 100% your job so when Apple announced that they were going to give
00:06:22 us compile time data rate safety I think the time I was as happy with that was probably when they said n will no longer crash your app right it didn’t in Objective C but I also did some Java at some point and in Java every other line is check that this is not null because otherwise things will crash in Swift you could actually encode that this thing might be nil or not and it’s very explicit eliminated a whole class of bugs and compile time data race protections would do the same thing for us and actors actually have this built
00:06:53 in right actors make sure that we don’t have data races um because the compiler will help us the actor will’ll make sure that everything is synchronized and it’s all really nice so when I talk about actors there’s a couple of terms that I use that we’re going to look at today we’re going to look at something called the actor mailbox actor re-entrancy and actor isolation re-entrancy I’m going to mostly skip over for today uh that’s because it’s I could probably do a whole talk on that alone what it
00:07:21 essentially means is that if an actor is asynchronously waiting for something to come back the actor is going to go ahead and do something else I’ll elaborate on that when we talk about the mailbox because you can’t understand re-entrancy if you don’t also talk a bit about mailbox anyway consider this date form matter cache it’s very similar to that uploader we saw before it’s got a private dictionary I have a function for format string returns a date formatter if we have one in the cache great we
00:07:46 return it if we don’t we make a new one configure it cache it and return it if we use that concurrently this crashes of course uh but if we use it normally and most of our code would look like this we ask the format cache for an instance we get it back and we can print dates it’s beautiful simple and convenient but unsafe so let’s say that we go ahead and fix that all we need to do is this we can change a single keyword go from a class to an actor boom we have safety it’s super simple super nice the problem
00:08:20 isn’t this the problem isn’t the definition of this actor the problem is that Usage Now becomes more complex because if I have that exact same code the compiler is now going to tell me that a call to an actor isolated instance method for matter cache in a synchronous non-isolated context and that I have to uh await that because it’s implicitly asynchronous so when you’re interacting with an actor from outside of the actor calling methods or accessing state is implicitly I put that in parenthesis
00:08:50 because it’s not that implicit but you didn’t make the thing async and it became async so it’s somewhat implicit um you’re not in the actor so you have to wait your turn because we’re now in Swift concurrency’s world so if I want to interact with this uh actor I have to await cache instance for matter you have to you can see I had to wrap this in a task so I’m now introducing concurrency where maybe I didn’t even want it in the first place so why is that needed well that’s where the actor mailbox comes in an
00:09:19 actor only does one thing at a time right I want you to keep that in mind an actor always only does one thing at a time so what happens is the actor has a so-called mailbox and we put function calls property access anything we want to do into that mailbox the first thing that goes in the actor sees that it’s eager to do work it goes okay I’ll I’ll get that form matter for you in the meantime other things concurrently start asking for it and the actor just at its own pace starts going through this now I mentioned re-entrancy what
00:09:48 happens with re-entrancy is the actor does one thing at a time once it starts to wait for something like a network call an actor loves to do work so it goes okay this network Call’s busy I’ll just pick up the next task in meantime until that comes back so it does one thing at a time but not automically just so you know what re-entrancy sort of is as a problem not the point of this talk the point is when you introduce actors you introduce concurrency because we have to await whatever we put into that mailbox so if
00:10:16 we want to ask for a formatter the actor might be doing something else at that time so we have to wait for it let’s talk a little bit about isolation because this is all about isolation in this case I have a a small actor token man manager is one property current user what we say is that current user is isolated to the actor manager uh to the Token manager and because it’s isolated to the Token manager when we want to get it out we have to wait for it so right here in my networking object which is
00:10:48 not isolated to anything it’s just a plain class we call that non-isolated it creates that manager let manager equals token manager current user code should be fine it’s not you got an error here saying sending user risks causing data races sending user to actor isolated initializer in its current user risks causing data races between actor isolated and local nonisolated uses this already tells us a lot about isolation because it is indeed saying current user and the initializer are both isolated to the actor token
00:11:25 manager which means that if the token manager owns that current user instance it wants to exclusively own it it wants to isolate it it wants to make sure that nobody does anything weird to it but our networking class also has a reference to it so at this point our non-isolated networking object and our isolated token manager both have access to user now we’ll talk about sendable later but user is not sendable this means that user is not safe to use in two isolation contexts at the same time right so that’s what’s sending user
00:11:58 to actual is olated initializer means we take it from a non-isolated place sending it to a place that is isolated these two are not friends so in Swift concurrency uh your code is always going to be either isolated to an actor or non-isolated there’s nothing in between either isolated to an actor or you’re not objects and state that exist within the same isolation context can be accessed freely this means that an actor can access its own methods without awaiting it it’s in the same isolation context an actor can access its own
00:12:31 State without awaiting it same isolation context also an actor is allowed to mutate its own State same isolation context when you pass objects or state between isolation contexts that is only allowed under certain conditions because that’s where the compiler goes hold on if two isolation contexts are able to access this this piece of State at the same time that might be a data Ras we don’t know what any of these isolation contexts will do with it but they might be doing something bad so the token manager has its own
00:13:04 isolation context and networking is non-isolated so when we pass current user from networking to our actor we cross what’s called an isolation boundary there’s several ways that an object can be isolated you’ve already seen actors actors have their own isolation context you can also use Global actors to isolate arbitrary objects methods and state to a shared actor instance this is the main actor for example you’ve probably seen at main actor added to a class or to a function or to a piece of state that’s what a
00:13:35 global actor is and that isolates that piece of state to one single main actor instance means that two unrelated objects can live in the same isolation context and they can talk to each other they’re friends a task inherits its current isolation context so whenever I make a new task oh if I make that task from a main actor isolated context that task also lives in the main actor context so let’s look at an example of using a global actor if I have this code right here an observable list view model
00:14:09 with a bunch of items in it and a load item async function and a view model inside of a view I get an error that looks like this sending main actor isolated self view model to non-isolated instance method load items risk causing data raises between non-isolated and main actor isolated uses it’s a variation of the error you saw before uh the first time I read this I looked at it and I out loud said to the compiler excuse me sending main actor isolated instance self view model I’m not I’m not sending it
00:14:43 anywhere it maybe into the task but okay I’ll work with that I’m sending it into the task fine two non okay not to the task apparently two non-isolated method load items I’m not doing that the view model is staying where it was before it’s staying in inside of the view so what are we talking about here it risks days races between non-isolated and main actor isolated use who thinks they have an idea of what this actually means Mr functional obious so what it means is this view this view is main actor isolated that’s because the view
00:15:20 protocol gained a main actor annotation so it is isolated to main we get that the task I create inherits isolation context so that task also runs on Main so that’s not my problem inside of the task I have access to the view model which is fine the task and the view model are main actor isolated so in that part we’re good load items is defined on a non-isolated class it’s a non-isolated async function it has access to self what does self point to the exact same instance of view model as I have in my
00:15:51 view so that does mean that I have access to that one instance from a non-isolated context inside of my view model and I have access in the isolated context inside of my task Okay cool so My Views are all isolated to the main actor view model instance therefore is also isolated to the main actor we call non-isolated Asing method on The View model it’s access from the main actor and itself so we know what’s wrong how do we fix that we could make sure that the views access is also nonisolated even if we could I don’t think we can it
00:16:31 sounds like it’s a terrible idea we could make the fuel model safe to be passed across isolation boundaries works but once you understand how sandable works that becomes kind of a problem because we have mutable States so that’s not an option or we could isolate the view model to the same actor as the view that’s not such a bad idea we could apply a main actor annotation to our view model and now that view model also is isolated so items is isolated Lo items is isolated they’re both isolated to the main actor just
00:17:01 like the view so now we can freely access uh self and the view model at the same time without problems because we’re all living in the main actor now do note that if you have an asynchronous operation inside of load items like maybe you’re awaiting a network call that doesn’t block the main actor I think it’s important to call that out awaiting something does never block the current actor in fact it allows the current actor to go ahead and be re-entered like we talked about an actor reentrancy and go do something
00:17:31 else so that it keeps uh itself busy and the network call actually just run somewhere else so that’s not a problem with the global actor you limit isolation context which is good you want to have as few of them as possible because it just makes your program simpler and it allows you to reduce concurrency and coincidentally reducing concurrency is precisely what the Apple team is currently like the Swift team is currently looking into they basically realize and they have a proposal up that they having us introduce so much
00:18:00 concurrency into our app a lot more than we probably want so they’re thinking about ways in which we can either automatically isolate things to the main actor if we call them for main or possibly even making your apps mainly main actor by default where you must explicitly leave so reducing concurrency Apple’s thinking about it is probably a good thing to do that however you might have multiple isolation boundaries and you could actually have state that’s unsafe safely cross over do you remember this we saw that a little
00:18:31 bit earlier like 2 minutes ago I hope you remember otherwise short ter memory is uh so if we pass that current User it’s actually a bit more than two minutes my memory might be bad uh so that that user is passed from the class networking which is nonisolated into our actor which is isolated so that’s not allowed because that user now lives in two isolation contexts and that user like I said was not safe to be passed across isolation boundaries however this code is fine I’m creating an instance of user inside of my
00:19:04 non-isolated create manager function so that user is definitely not isolated to anything I’m passing it into my token manager the compiler accepts this since Swift 6 that’s a swift 6 compiler not the language mode so you can have the Swift 5 language mode Swift 6 compiler and use this this is a feature called region based isolation and with region based isolation the compiler can see that our user starts off in a create manager isolation context which is in this case non-isolated it can then see that we
00:19:36 pass it to the Token manager isolation context which is a new one so it’s now transferred from A to B and it also sees that we never use it again so basically compiler says okay you created this in a it’s technically not safe to go to B but if a never touches it again I’ll allow it so the compiler knows that this is okay because users only ever used in one isolation context at a time have you ever tried to do this in Swift 5 it’s pretty much the same thing as we had before and if you have then a
00:20:11 compiler would tell you something about capturing non- sendable state in a sendable closure the pattern looks familiar doesn’t it it’s pretty much the same thing so you would hope that region based isolation that we had here would also apply in the case of a task here sadly it doesn’t um in Swift 5 task took a sendable closure so that’s slightly different from being able to apply region based isolation if I mimic this and I Define a variable that is a sendable closure and I capture a user in there I get my error
00:20:45 capture of user with non- sendable type user in a sendable closure when compiled with swift 6 however in Swift six we are allowed to do this with tasks and they made some changes to task continuations and more but to get that we’re going to move into our final pillar of concurrency and that is sendable so keep that hold that thought on why we are allowed to uh cross isolation boundaries with task now when an object is sendable we know that it is safe to be used concurrently we know that it is allowed to cross
00:21:17 isolation boundaries we know that no matter what we do with it no matter how concurrent we are with that object it should never have a data race the object can be passed across isolation boundaries without problems and multiple isolation boundaries can hold a reference to that object and the compiler will enforce this for us right if something isn’t sendable the compiler will tell us hey you’re doing something with uh this object and it’s only allowed if that object were sendable the cool thing is actors are always
00:21:45 sendable because of their own isolation context because of them protecting uh their state from data races so that’s nice other objects can also be sendable a struct for example is sendable if all of its members are sendable it conforms to the sendable protocol which you don’t have to do explicitly if the struct is internal only public struct must be explicitly made sendable and that’s actually a really good thing uh because if you have a public struct that just by accident happens to be sendable you
00:22:13 don’t want that to suddenly be part of your public API contract because now you’re stuck making that struct sendable forever and that might never have been your intention so a sendable struct an example class user completely not sendable our first struct right here it has a let ID of type uu ID uu ID is sendable and a VAR count of type int int is sendable the fact that we have mutable State on this struct is completely fine if we pass this struct from one isolation context to the next each one has its own copy so making
00:22:43 changes to the count does not impact the other copy refine the second struct has only let properties but it has a reference type user which is not sendable as one of its members so the compiler is going to tell you that is not sendable en are sendable if they have no Associated values if you just have enum with a list of cases that’s fine that’s sendable if you do have Associated values those Associated values must be sendable themselves and you must conform it to sendable so in example again the top
00:23:15 enum is safe it has an Associated value on its user case of sendable struct name implies that’s sendable so we’re good the second one has a case user with the class user which was not sendable so the compiler is going to tell us that is not okay classes are a special creature they can be sendable if all of their members are sendable we’re kind of used to that by now they can’t have any mutable State because they’re reference types so if we pass them to two isolation boundaries and both can be mutating a VAR then we
00:23:47 might have a data race so we can only have lets on our sendable class the class must be final so we can’t be inherited from anymore this is the end of the line and we must confirm it ascendable by hand if we meet all these criteria the class can be sendable so example time top class is sendable two let properties uid ends we’re good bottom one two let properties but one of them is user so we’re not good what if my class is threat safe because I’m refactoring my code I know that this class is completely threat
00:24:20 safe I did everything I could I I used logs dispatch cues semaphor everything uh it’s fine I have tests I listen to Kristoff but it doesn’t apply like it has fars I can use unchecked sendible so you can make anything you want sendable with this and you should totally do it no don’t do it it makes sense to use it sometimes if you know that the class is thread safe and you have to test to prove it and you intend to revisit and adjust your class at a later time and really you should only use a temporary it makes full sense
00:24:55 in my opinion to do this while you’re refactoring you can’t refactor every object at the same time but this should not stick in your code base for a really long time so that’s types we also have sendable closure and ascendable closure is safe to run from any isolation context concurrently as you might imagine and that means that it’s only allowed to capture sendable state do you see where we’re going with that one we had the variable in the beginning with the sendable closure and it captured some states if we Define or accept a
00:25:24 sendable closure we can do this VAR my closure is of type sendable function that takes no arguments is async throws and returns void or we can have a function that takes an argument of the same type ascendable closure can be async it can be throwing it can be both or it can be neither okay so functions can take async throwing function uh async throwing sendable closures or they can take plain sendable closures but in a lot of situations a sendable closure if you’re writing an API that takes a closure that you want
00:25:55 to run uh in a different isolation context a sendable closure is probably not what you intend to receive because as we sort of discussed earlier this code is technically safe I’m creating an instance of user passing it into a sendable closure but the compiler won’t let me do that because user is not sendable to fix that the compile Swift 6 introduces sending sending is a new keyword as if we hadn’t enough keywords in Swift uh so we have another one sending and we can uh now pass a user instance that we created inside of our
00:26:30 function into as sending closure and this is very similar to region based isolation in the sense that the compiler is going to make sure that this user that we pass from one isolation context into that closure is never used after we send it right so let’s explore that a little bit tasks now take send enclosures continuations use sending closures and task rep you sending closures so that’s the case since Swift 6 and that’s why you could pass a non sendable user into a task now and probably if you have closures
00:27:04 that take sendable uh functions that take sendable closures you should try and make them sending it’s simply a little bit more convenient to work with from the call site and it’s just as safe because the compiler helps us here in this case we would get an error because I have my let user I pass it to ascendable closure and then I try to print its name the compiler is going to tell me no you can’t do that because value of non sendable typ colleag guaranteed return void returns nothing accessed after being transferred later
00:27:33 access could raise best compiler error ever um luckily this is on the lines where you’re using it and especially that last one access can happen concurrently that should tell you hold on a second I’m doing something that is sending and that’s not okay the compiler now knows that run sendable example and that last print could actually run concurrently and that might be a problem so the next time you see something like this sending closure as a sending parameter risk causing data races between code and the current task and 00:30:36 they seem to be leaning into so and also don’t make everything an actor like it might be tempting at some point to go you know what I’m going to embrace concurrency all the way everything’s an actor now just let’s go and again just to drive that home try and reduce concurrency rather than increase it it is a tool to be used when it makes sense not everything in your app should be concurrent thank you very much [Applause]
00:28:06 concurrent execution of the closure you know this is probably related to the fact that I’m using user inside of my task and outside of my task the user got captured in the task probably was an instance member so it wasn’t allowed anyway but now you know that that’s probably related to that or you see sending main Act isolated self view model to non-isolated instance method load items you now know that view model belongs to the main actor load items is nonisolated but it has access to self so now your view
00:28:39 model is accessed from the main actor and the non-isolated context and that’s the problem and you know that your view models might need to be main actor annotated or if you see this one non sendable type movie returned by implicitly asynchronous call a non-isolated function cannot cross actor boundary you now know that that function is main actor isolated load movies is async and non-isolated movie is not sendable but it’s created in a non-isolated context you send it to something that’s isolated
00:29:07 hey that’s not okay how do I solve it well that’s a different problem at least you understand why you have the problem or if you see this static property shared is not concurrency safe because non sendable type dates from met cach may have shared mle State you now know that apparently you’re doing something that isn’t sendable maybe that date form matter C should be sendable maybe you should make it an actor so how do you deal with all these compiler errors first of all if you see a compiler error related to sending
00:29:39 something or something passing isolation boundaries ask yourself whether you intended to introduce concurrency when your view model tells you that you’re doing something that isn’t isolated and you now have a concurrency problem you probably never intended for that function to be async Market main actor it’s completely fine if you’re not not sure about that try to trace something that doesn’t use concurrency put some thread is main all the way from your view to your networking code I’m willing to bet that in most cases you don’t
00:30:07 leave the main actor until right before you make the network call so you’re probably okay with the main actor in a lot of places and then figure out the smallest change that makes sense don’t refactor everything the more you refactor the more problems you might probably introduce and then think really hard about your intentions again did you really want concurrency here really don’t be afraid to run that app code on the main actor it’s probably where it belongs Apple wants it to be the default probably most likely is what