EP 357 · Isolation · Mar 9, 2026 ·Members

Video #357: Isolation: What Is It?

smart_display

Loading stream…

Video #357: Isolation: What Is It?

Episode: Video #357 Date: Mar 9, 2026 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep357-isolation-what-is-it

Episode thumbnail

Description

What is “isolation” in Swift? We define the term based on Swift’s open “evolution” process and flesh out an example from the proposal to get an understanding of its purpose, and see how legacy tools can lead to dangerous situations at runtime.

Video

Cloudflare Stream video ID: 0215fb5186e8a6c1f918de0fb4a21b30 Local file: video_357_isolation-what-is-it.mp4 *(download with --video 357)*

References

Transcript

0:05

More than 3 years ago we did a deep dive into Swift’s, then new, concurrency tools. Those were some of our most popular episodes ever, and they approached the subject through the lens of the past, present and future of concurrency tools. We showed how these tools have evolved from locks, to dispatch queues, to reactive streams, and finally to structured concurrency and async await. Everything we covered in those episodes is extremely important, and we highly recommend you watch those episodes, but also a lot has changed since then. Stephen

0:36

That’s why we are excited to start a brand new series dedicated to the concept of “isolation” in Swift. This is by far the most important concept to understand when it comes to concurrency, yet is probably the least understood throughout our community. Isolation is a compile-time guarantee that when a particular line of code is executed it will be free from data races or data corruption. The most amazing part of this is that we don’t have to take any special considerations into account when writing these lines of code, such as using locks or mutexes. The mere fact that the Swift compiler has deep knowledge of the concept of “isolation”, and the fact that Swift provides tools for creating isolation domains, we get to write completely boring and vanilla code exactly how we want without ever worrying about data races or synchronization. Brandon

1:21

However , when approached naively, isolation in Swift spirals out of control into a world of asynchrony, forcing you to sprinkle await s throughout all of your code, and forcing you to make things async that should not be async at all. For example, the most fundamental tool for creating an isolation domain in Swift is the actor, and actors use the concurrency runtime to enforce isolation. When you want to access something inside an actor from the outside, you almost always have to await so that exclusive access to the data can be guaranteed, and then you are allowed access to the data. Stephen

1:56

And because of this many think the Swift isolation is more trouble than it’s worth. If isolating your data means you now need to make everything async in your app, then that seems like a really bad tradeoff. Most code we write day-to-day is perfectly fine being synchronous, and synchronous code is far easier to maintain. Brandon

2:11

Well, we are here to say it doesn’t have to be that way. Once you understand the full arsenal of isolation tools offered by Swift, it is possible to embrace isolation while greatly reducing the need to make everything async and embracing synchronous code. And further, when it really is absolutely necessary to introduce async work, it is possible to do in a way that captures and propagates isolation, making it much more understandable and controllable. Stephen

2:40

And once you start taking the time to do this, all types of extra benefits start popping up. For example, when this is all done correctly you suddenly don’t need to mark everything under the sun as Sendable because you won’t be passing data across isolation boundaries as much. And it may sound counterintuitive to say, but Sendable is actually quite a viral protocol and there is a lot of power in avoiding it. Brandon

3:01

And once we cover all of this material we will be able to see at the very end that when isolation is dealt with correctly, it performs much better than tools such as locks. There’s a bit of misinformation floating around the Swift community that locks are more performant than things like actors, but the examples to demonstrate this are just completely wrong. So it will be fun to debunk some of those claims.

3:24

Now let’s begin with properly defining isolation so that we are all on the same page when we use that term, and take a quick look at the tools that provided isolation prior to Swift actors.

3:37

Let’s get started. What is isolation?

3:39

The first real mention of isolation as a dedicated tool for preventing data races is in the Swift Evolution proposal #306 , which introduced the concept of actors:

3:51

Technically there were mentions of “isolation” in a few proposals just before this, like in #296 and #302, which introduced the concept of sendability and suspension points, but those proposals only mention the term a handful of times, mostly in reference to future proposals coming, whereas proposal #306 mentions isolation a whopping 45 times.

4:12

There isn’t just a single place in this document we can point to to say here is the official, rigorous definition of isolation, but we can piece together a few things. It starts with this paragraph: Note Each actor protects its own data through data isolation , ensuring that only a single thread will access that data at a given time, even when many clients are concurrently making requests of the actor.

4:41

So actors protect their data through isolation, and isolation means that somehow only a single thread at a time is allowed to access the data inside the actor.

4:50

Then a few paragraphs below, when comparing actors to other kinds of data types in Swift, such as structs and classes, it says: Note The primary difference is that actors protect their state from data races. This is enforced statically by the Swift compiler through a set of limitations on the way in which actors and their instance members can be used, collectively called actor isolation .

5:15

OK, so not only is isolation a concept that relates to protecting data, it is a statically enforced rule by the compiler. Somehow the compiler is able to know when data is isolated and what are the appropriate times to allow access to that data. And if you ever try to access the data in an inappropriate fashion, it becomes a compile-time error.

5:37

And the final paragraph that ties a bow on the nebulous definition of isolation forming is the following implementation note just a few paragraphs below: Implementation note At an implementation level, the messages are partial tasks (described by the Structured Concurrency proposal) for the asynchronous call, and each actor instance contains its own serial executor (also in the Structured Concurrency proposal). The default serial executor is responsible for running the partial tasks one-at-a-time. This is conceptually similar to a serial DispatchQueue , but with an important difference: tasks awaiting an actor are not guaranteed to be run in the same order they originally awaited that actor. Swift’s runtime system aims to avoid priority inversions whenever possible, using techniques like priority escalation. Thus, the runtime system considers a task’s priority when selecting the next task to run on the actor from its queue. This is in contrast with a serial DispatchQueue, which are strictly first-in-first-out. In addition, Swift’s actor runtime uses a lighter-weight queue implementation than Dispatch to take full advantage of Swift’s async functions.

5:52

Each actor has what is known as a “serial executor” under the hood, and that is what actually enforces this isolation principle. One cannot simply access the data inside an actor from the outside. Instead, one requests access, via the await keyword, and that puts a message in a queue, and the serial executor goes through that queue to execute the messages whenever it is not currently processing anything.

6:21

And it’s worth noting that threads are heavily de-emphasized in this proposal. Out of the more than 8,500 words to propose actors, only 5 of them are the word “thread”, and none of those mentions are substantially about theads. Swift’s concurrency model does not want us to think about threads. And so isolating data does not necessarily mean make the data accessible only from one particular thread. The data can be accessed from multiple threads just fine, but never simultaneously.

6:54

This implementation note calls out something else very important. The way I just described things sounds very similar to dispatch queues. You can enqueue as many units of work onto the queue as you want, and then queue just goes down the line executing them one-by-one. However, executors get one additional super power, which is the ability to execute the messages out of order depending on priority. Without this feature it would be possible to enqueue a low priority message, followed by a whole bunch of high priority messages, and those high priority messages would have to wait around until the system decides it’s time to execute the low priority one. This is known as priority inversion, and it is a problem that plagues grand central dispatch unless you are very careful. The problem with non-isolated code

7:50

OK, we’ve just done a lot of talking without any coding. But the main thing we just learned is that it appears that actors are inextricably tied to the notion of isolation in Swift. They create a whole new isolation domain for data to safely rest inside, and the compiler is able to guarantee that two threads will never access the data at the same time.

8:12

And we are soon going to really poke at these ideas by showing concretely what these isolation domains look like at runtime, and how one passes data back and forth between domains and even how one can merge isolation domains together so that actors can communicate with each other in a synchronous fashion without suspension points. Stephen

8:32

But, before doing that we want to quickly show what it would look like to emulate the concept of isolation using older tools. In particular, locks existed long before actors did, and they are a tool for creating isolation, in their own way. And so do we need actors at all? Can’t locks do everything actors can do? Well, actually not at all!

8:50

To demonstrate this we are going to make use of an example that has been used in WWDC talks, evolution proposals, and the broader Swift community has used this example a bunch too. But, none of those sources took the example far enough to really see how complicated isolation can be.

9:05

Let’s begin.

9:08

Going back to the actors proposal we will see they demonstrate the concept with a BankAccount actor: actor BankAccount { let accountNumber: Int var balance: Double init(accountNumber: Int, initialDeposit: Double) { self.accountNumber = accountNumber self.balance = initialDeposit } }

9:13

This actor just holds some basic data, and then later in the document they extend the type with a transfer method for transferring some money from one account to another: extension BankAccount { func transfer(amount: Double, to other: BankAccount) async throws { assert(amount > 0) if amount > balance { throw BankError.insufficientFunds } print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)") // Safe: this operation is the only one that has access to the // actor's isolated state right now, and there have not been any // suspension points between the place where we checked for // sufficient funds and here. balance = balance - amount // Safe: the deposit operation is placed in the other actor's // mailbox; when that actor retrieves the operation from its // mailbox to execute it, the other account's balance will get // updated. await other.deposit(amount: amount) } }

9:35

This very simple example demonstrates how to protect some mutable state inside its own little isolation domain, and the compiler even enforces the isolation by disallowing access to the state outside of the actor.

9:45

This example is pretty good, but it’s also too simplistic to see just how complex isolation domains can be in the real world. We are going to use this as inspiration for a more complex example involving multiple objects wanting to communicate with each other.

9:58

I have a project open right now that is just a simple SPM package, and there is a test target. We are going to use tests to explore these ideas because it gives us the ability to not only see the compile-time ramifications of isolation, but we can also run tests to prove that isolation works the way we expect.

10:14

I am going to create a new file called Bank.swift, and in that file I will start a Account class to mimic what the actor is doing from the proposal. The class will have an ID and a balance: class Account: Identifiable { let id: UUID var balance: Int init(id: UUID, balance: Int = 0) { self.id = id self.balance = balance } }

10:25

In the example from the actors proposal, a transfer method is defined directly on this account type to transfer an amount from one account to another, and further it even needed to be async in order to communicate between actors.

10:39

Well, I don’t think that’s very realistic. It is far more likely to have another object that manages a collection of accounts and can facilitate things like transfers. We will call this object a bank: class Bank { }

10:52

And a bank will hold onto a collection of accounts, which we will represent as a dictionary keyed by the account ID: private var accounts: [Account.ID: Account] = [:]

11:04

Now this object is in a better position to facilitate transfers between accounts because it has access to all accounts at once.

11:09

Let’s sketch the signature of a function that aims to transfer a fixed amount from one account specified by an ID, to another account specified by an ID: func transfer( amount: Int, from fromID: Account.ID, to toID: Account.ID ) { }

11:29

The first thing we will do is try to look up the from and to accounts. If we can’t find them it would probably be best to throw an error: guard let fromAccount = accounts[fromID], let toAccount = accounts[toID] else { struct AccountNotFound: Error {} throw AccountNotFound() }

11:53

Once we have the accounts we will want to make sure that the “from” account has enough balance to cover the transfer, and if not throw another error: guard fromAccount.balance >= amount else { struct InsufficientFunds: Error {} throw InsufficientFunds() }

12:11

And if we get past all of these guards, we just need to subtract the amount from the “from” account and add it to the “to” account: fromAccount.balance -= amount toAccount.balance += amount

12:19

This is all it takes to implement this method. It’s not the most concise way to implement this method, and we will be improving that aspect soon enough, but there is something much bigger wrong with this code.

12:29

In order to see this we are going to write some tests. Let’s get a very basic test suite in place: @Suite struct BankSuite { @Test func basics() throws { } }

12:37

Let’s create a bank: let bank = Bank()

12:40

And let’s open two accounts in this bank so that we can test transfer money between them. Well, we don’t actually have a way to open accounts currently. Let’s add a method that can open an account with an initial deposit: func openAccount(initialDeposit: Int = 0) -> Account.ID { let id = UUID() accounts[id] = Account(id: id, balance: initialDeposit) return id }

13:15

Now we open up two accounts with a balance of 100, and then transfer 50 from the first account to the second: let id1 = bank.openAccount(initialDeposit: 100) let id2 = bank.openAccount(initialDeposit: 100) try bank.transfer(amount: 50, from: id1, to: id2)

13:36

We have now completed the transfer, but how can we assert that everything worked successfully? So far everything is quite hidden and opaque in the bank.

13:44

One thing we could do is add a property to the bank for summing up total deposits: var totalDeposits: Int { accounts.values.reduce(into: 0) { $0 += $1.balance } }

14:05

And then we could check that the total deposits is still 200: #expect(bank.totalDeposits == 200)

14:14

This test passes, and it at least shows something , but we don’t yet know that 50 was actually transferred from the first account to the second.

14:23

I guess we really have no choice but to allow fetching an account from the bank given an ID: func account(for id: Account.ID) throws -> Account { guard let account = accounts[id] else { struct AccountNotFound: Error {} throw AccountNotFound() } return account }

14:51

And now we can confirm that the first account has a balance of 50 and the second 150: #expect(try bank.account(for: id1).balance == 50) #expect(try bank.account(for: id2).balance == 150)

15:15

This test passes, and so we can be sure that the basics of transferring does work properly.

15:20

So, it’s great we have a passing test, but things are far from perfect with our bank and account classes. To see this, let’s write a test that emulates a rush on the bank to open 1,000 accounts concurrently: @Test func newAccountRush() async throws { let bank = Bank() await withTaskGroup { group in for _ in 1...1000 { group.addTask { bank.openAccount(initialDeposit: 100) } } } }

15:55

By the end of this rush we would expect 1,000 accounts with a balance of 100: #expect(bank.totalDeposits == 100 * 1000)

16:08

But unfortunately when we run this test we get a crash: Task 19: EXC_BAD_ACCESS (code=1, address=0x8000000000000008)

16:12

If we look at the debug navigator, we should be able to find two threads that are accessing the accounts shared state at the same time:

16:25

This is an example of a “data race”. Two threads are accessing a piece of mutable state at the same time. Sometimes this leads to data corruption, where threads interleave in a way that causes old data to be written over newer data. And other times it causes a runtime crash, as we are seeing here. Next time: Locking Stephen

16:41

So, we just now saw that it’s very easy to write code that is susceptible to data races, which means two threads access a single piece of state at the same time. This often leads to corrupt data, where stale data is written, but can also lead to outright crashes.

16:53

The goal of Swift’s concurrency tools is that a program compiled with the strictest concurrency settings should be free from data races as long as you aren’t using any of the escape hatches that are available. Brandon

17:03

There is another closely related concept to data races known as “race conditions”. It’s a broader category of concurrency bugs than data races. Every data race is a race condition, but not every race condition is necessarily a data race. A race condition is just a logical error due to executing your code on multiple threads, and it can happen even if there is never an instant where literally 2 threads are accessing a single piece of data at the same time.

17:30

So, why is Swift letting us write this kind of code? Isn’t it supposed to prevent us from doing this with its fancy concurrency checking? Well, we have purposely put our SPM package in Swift 5 mode, which turns off all concurrency checking. So it shouldn’t be too surprising we were allowed to write unsafe code without any warnings or errors.

17:48

Let’s now turn on Swift 6 mode, and see what to do about fixing this crash…next time! References SE-0306: Actors John McCall, Doug Gregor, Konrad Malawski, Chris Lattner • Oct 30, 2020 This proposal introduces actors into Swift. An actor is a reference type that protects access to its mutable state.” https://github.com/swiftlang/swift-evolution/blob/main/proposals/0306-actors.md Downloads Sample code 0357-beyond-basics-isolation-pt3 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .