Making a basic network call
let (data, response) = try await URLSession.shared.data(from: someURL)
But looks can be deceiving...
!
Hi, I'm Donny
donnywals.com 2023
donnywals.com 2023
Your
!
on Swift Concurrency
donnywals.com 2023
Making a basic network call
let (data, response) = try await URLSession.shared.data(from: someURL)
So we should always make sure to
await on a background thread,
right?
I don't know, should we?
You said the await means that we
suspend execution. So aren't we
blocking our thread?
An await does not block your
current thread. It allows
suspension of the current
task.
While we're suspended,
other tasks can make
progress
At this point in a workshop,
there's almost always one
person that I see thinking
really hard.
Threading in a non-concurreny
context
func someVerySlowOperation() {
// this takes a while...
}
DispatchQueue.main.async {
someVerySlowOperation()
}
Threading in a non-concurreny
context
func someVerySlowOperation() {
// this takes a while...
}
DispatchQueue.global().async {
someVerySlowOperation()
}
The same code but with Swift
Concurrency
func someVerySlowOperation() async {
// this takes a while...
}
await MainActor.run {
await someVerySlowOperation()
}
Uhmm....
So, how can we know?
Should we use Task or
Task.detached to be sure?
What does this really mean?
func someVerySlowOperation() async {
// this takes a while...
}
Task.detached {
await someVerySlowOperation()
}
This doesn't change
anything;
someVerySlowOperation still
runs in the same way.
The system does not care
where you call something
from
Functions run on:
1. Main Actor (Main thread)
2. Global executor (Every thread except main)
So how do we decide or
determine where a certain
function will run?
This function runs on the global
executor
class MyViewModel: ObservableObject {
func performSomeWork() async {
// this will always run on the Global executor
}
}
A practical example...
struct ContentView: View {
@StateObject var vm = MyViewModel()
var body: some View {
Button {
Task {
await performSomeWork()
}
} label: {
Text("Test")
}
}
}
Changing the execution context
class MyViewModel: ObservableObject {
@MainActor func performSomeWork() async {
// this will always run on the Main actor / thread
}
}
But things can get tricky...
This is much harder to reason about
@MainActor
class MyViewModel: ObservableObject {
func performSomeWork() async {
// this will always run on the Main actor / thread
}
}
So did that make the await blocking?
Opting out of main actor isolation
@MainActor
class MyViewModel: ObservableObject {
nonisolated func performSomeWork() async {
// this will _not_ run on the Main actor / thread
}
}
Async functions will run on
the global executor unless
they were specifically
instructed to not do that
So here's an even trickier one for
you...
struct ContentView: View {
@StateObject var myViewModel = MyViewModel()
var body: some View {
Button("Test") {
Task {
await someExpensiveOperation()
}
}
}
private func someExpensiveOperation() async {
// where does this function run?
}
}
SwiftUI views get an implicit
main actor annotation when
they hold an observable
object...
So... what can we take away from
this?
• Key rule: Functions run on the global executor unless otherwise
specified.
• Mark method or enclosing objext as @MainActor to enforce main
actor
• Use nonisolated to break free specific methods
• SwiftUI views with observable objects are implicitly @MainActor
So what about Tasks?
What good are they if they don't
affect where stuff runs?...
Creating an unstructured task
Task {
// I'm an unstructured task
}
Creating a detached task
Task.detached {
// I'm a detached task
}
We can make our task produce a
result too:
let task = Task {
return await someOperation()
}
let value = await task.value
What happens when we create a
task...
• Unstructured tasks inherit parts of their creation context
• Unstructured tasks are not child tasks of their creation context
• Detached tasks inherit noting
• Both tasks are their own islands of concurrency
So when do we use each?
• As little as possible
• Generally speaking we should use unstructured tasks
• Detached should only be used as a last resort
What about structured
concurrency?
It's one of the few actually
recently "invented"
paradigms in programming
It's one of the few actually
recently "invented"
paradigms in programming
Even though it's based on something from the 60's...
Structured concurrency
relates to tasks and their
children
Fetching multiple things
concurrently
func fetchUserInformation() async throws -> Profile {
let user = try await userProvider.currentUser()
async let favorites = userInfoProvider.favorites(for: user)
async let friends = userInfoProvider.friends(for: user)
async let posts = userInfoProvider.posts(for: user)
return try await Profile(for: user, favorites: favorites,
friends: friends, posts: posts)
}
Structured concurrency as a graph
Because this is structured
concurrency, the parent task
cannot complete before its
children complete.
Let's see what happens
when we add an
unstructured task
Structured Concurrency...
Let's talk about actors real
quick
Actors synchronize access to their
(mutable) state
actor DateFormatters {
private var formatters: [String: DateFormatter] = [:]
func formatter(using dateFormat: String) -> DateFormatter {
if let formatter = formatters[dateFormat] {
return formatter
}
let newFormatter = DateFormatter()
newFormatter.dateFormat = dateFormat
formatters[dateFormat] = newFormatter
return newFormatter
}
}
Concurrent calls to
formatter(using:) will be
serialized
So an actor is like a serial
queue then?
Well, no...
A suspended actor is not
locked
actor DataProcessor {
var items = Set<Item>()
func processItem(_ item: Item) async {
guard items.count < 10 && !items.contains(item) else {
return
}
let result = await upload(item)
// items might now contain the item
// items might now have a count > 10
items.insert(item)
}
}
In Summary
• Building a mental model around what runs where isn't trivial
• Await is not a blocking operation
• Functions run on the global executor unless instructed otherwise
• Create tasks only when needed
• Strucured concurrency describes the relationship between
parent and child tasks
• Actors are not like serials queues

Your 🧠 on Swift Concurrency

  • 2.
    Making a basicnetwork call let (data, response) = try await URLSession.shared.data(from: someURL)
  • 3.
    But looks canbe deceiving...
  • 4.
  • 5.
  • 6.
  • 7.
    Making a basicnetwork call let (data, response) = try await URLSession.shared.data(from: someURL)
  • 8.
    So we shouldalways make sure to await on a background thread, right?
  • 9.
    I don't know,should we?
  • 10.
    You said theawait means that we suspend execution. So aren't we blocking our thread?
  • 11.
    An await doesnot block your current thread. It allows suspension of the current task.
  • 19.
    While we're suspended, othertasks can make progress
  • 20.
    At this pointin a workshop, there's almost always one person that I see thinking really hard.
  • 21.
    Threading in anon-concurreny context func someVerySlowOperation() { // this takes a while... } DispatchQueue.main.async { someVerySlowOperation() }
  • 22.
    Threading in anon-concurreny context func someVerySlowOperation() { // this takes a while... } DispatchQueue.global().async { someVerySlowOperation() }
  • 23.
    The same codebut with Swift Concurrency func someVerySlowOperation() async { // this takes a while... } await MainActor.run { await someVerySlowOperation() }
  • 24.
  • 25.
    So, how canwe know? Should we use Task or Task.detached to be sure?
  • 26.
    What does thisreally mean? func someVerySlowOperation() async { // this takes a while... } Task.detached { await someVerySlowOperation() }
  • 27.
  • 28.
    The system doesnot care where you call something from
  • 29.
    Functions run on: 1.Main Actor (Main thread) 2. Global executor (Every thread except main)
  • 30.
    So how dowe decide or determine where a certain function will run?
  • 31.
    This function runson the global executor class MyViewModel: ObservableObject { func performSomeWork() async { // this will always run on the Global executor } }
  • 32.
    A practical example... structContentView: View { @StateObject var vm = MyViewModel() var body: some View { Button { Task { await performSomeWork() } } label: { Text("Test") } } }
  • 33.
    Changing the executioncontext class MyViewModel: ObservableObject { @MainActor func performSomeWork() async { // this will always run on the Main actor / thread } }
  • 34.
    But things canget tricky...
  • 35.
    This is muchharder to reason about @MainActor class MyViewModel: ObservableObject { func performSomeWork() async { // this will always run on the Main actor / thread } }
  • 36.
    So did thatmake the await blocking?
  • 37.
    Opting out ofmain actor isolation @MainActor class MyViewModel: ObservableObject { nonisolated func performSomeWork() async { // this will _not_ run on the Main actor / thread } }
  • 38.
    Async functions willrun on the global executor unless they were specifically instructed to not do that
  • 39.
    So here's aneven trickier one for you... struct ContentView: View { @StateObject var myViewModel = MyViewModel() var body: some View { Button("Test") { Task { await someExpensiveOperation() } } } private func someExpensiveOperation() async { // where does this function run? } }
  • 40.
    SwiftUI views getan implicit main actor annotation when they hold an observable object...
  • 41.
    So... what canwe take away from this? • Key rule: Functions run on the global executor unless otherwise specified. • Mark method or enclosing objext as @MainActor to enforce main actor • Use nonisolated to break free specific methods • SwiftUI views with observable objects are implicitly @MainActor
  • 42.
    So what aboutTasks? What good are they if they don't affect where stuff runs?...
  • 43.
    Creating an unstructuredtask Task { // I'm an unstructured task }
  • 44.
    Creating a detachedtask Task.detached { // I'm a detached task }
  • 45.
    We can makeour task produce a result too: let task = Task { return await someOperation() } let value = await task.value
  • 46.
    What happens whenwe create a task... • Unstructured tasks inherit parts of their creation context • Unstructured tasks are not child tasks of their creation context • Detached tasks inherit noting • Both tasks are their own islands of concurrency
  • 47.
    So when dowe use each? • As little as possible • Generally speaking we should use unstructured tasks • Detached should only be used as a last resort
  • 48.
  • 49.
    It's one ofthe few actually recently "invented" paradigms in programming
  • 50.
    It's one ofthe few actually recently "invented" paradigms in programming Even though it's based on something from the 60's...
  • 51.
    Structured concurrency relates totasks and their children
  • 52.
    Fetching multiple things concurrently funcfetchUserInformation() async throws -> Profile { let user = try await userProvider.currentUser() async let favorites = userInfoProvider.favorites(for: user) async let friends = userInfoProvider.friends(for: user) async let posts = userInfoProvider.posts(for: user) return try await Profile(for: user, favorites: favorites, friends: friends, posts: posts) }
  • 53.
  • 58.
    Because this isstructured concurrency, the parent task cannot complete before its children complete.
  • 59.
    Let's see whathappens when we add an unstructured task
  • 62.
  • 63.
    Let's talk aboutactors real quick
  • 64.
    Actors synchronize accessto their (mutable) state actor DateFormatters { private var formatters: [String: DateFormatter] = [:] func formatter(using dateFormat: String) -> DateFormatter { if let formatter = formatters[dateFormat] { return formatter } let newFormatter = DateFormatter() newFormatter.dateFormat = dateFormat formatters[dateFormat] = newFormatter return newFormatter } }
  • 65.
  • 66.
    So an actoris like a serial queue then?
  • 67.
  • 68.
    A suspended actoris not locked
  • 69.
    actor DataProcessor { varitems = Set<Item>() func processItem(_ item: Item) async { guard items.count < 10 && !items.contains(item) else { return } let result = await upload(item) // items might now contain the item // items might now have a count > 10 items.insert(item) } }
  • 70.
    In Summary • Buildinga mental model around what runs where isn't trivial • Await is not a blocking operation • Functions run on the global executor unless instructed otherwise • Create tasks only when needed • Strucured concurrency describes the relationship between parent and child tasks • Actors are not like serials queues