Swift Concurrency Notes
In this article, I will share my insights about Donny Wals's presentation in iOS Conf SG 2023.
@MainActor
-
The @MainActor attribute in Swift is used to ensure that certain parts of your code always run on the main thread. This is particular useful in SwiftUI, where UI updates must be performed on the main thread.
-
SwiftUI views are implicit @MainActors when they have @ObservableObject properties like @StateObject, @ObservedObject and @EnvironmentObject
struct MyView: View {
@StateObject var vm = MyViewModel()
var body: some View {
Button {
Task {
await performSomeWork()
}
} label: {
Text("Test")
}
}
}
func someVerySlowOperation() async {
// this takes a while...
}
await MainActor.run {
await someVerySlowOperation()
}
- How can we know the current task is not on main thread?
class MyViewModel: ObservableObject {
// this will always run on the Global executor
func performSomeWork() async {
}
}
- Changing the execution context
class MyViewModel: ObservableObject {
// this will always run on the Main actor / thread
@MainActor func performSomeWork() async {
}
}
- Opting out of MainActor isolation
@MainActor
class MyViewModel: ObservableObject {
// this will _not_ run on the Main actor / thread
nonisolated func performSomeWork() async {
}
}
-
Async functions will run on the global executor unless they were specifically instructed to not do that
-
Trickier example:
struct ContentView: View {
@StateObject var myViewModel = MyViewModel()
var body: some View {
Button("Test") {
Task {
await someExpensiveOperation()
}
}
}
**Where does this function work?**
// Since usage of @StateObject, @ObservedObject and @EnvironmentObject properties
// Struct gains implicit MainActor behaviour.
// So, the answer is this function also work on **Main actor / thread**
private func someExpensiveOperation() async {
}
}
Recap:
🌟 Key rule: Functions run on the global executor unless otherwise specified.
🌟 Mark method or enclosing object as @MainActor to enforce main actor
🌟 Use nonisolated to opt out MainActor isolation
🌟 SwiftUI views with ObservableObjects are implicitly @MainActor.
Task
In Swift Concurrency, a Task represents a unit of work. There are two types of tasks: unstructured and detached. Unstructured tasks inherit parts of their creation context, but they are not child tasks of their creation context. Detached tasks, on the other hand, do not inherit anything. Both types of tasks can work in parallel with other tasks and do not interact with other tasks.
-
Creating an unstructured Task
Task { // I'm an unstructured task }
-
Creating a detached Task
Task.detached { // I'm a detached task }
-
What happens when we create a Task…
🌟 Unstructured tasks inherits parts of their creation context.
🌟 Unstructured tasks are not child tasks of their creation context.
🌟 Detached tasks inherit nothing.
🌟 Both tasks are their own islands of concurrency. They can work in parallel other with concurrent tasks. They don’t interact with other tasks which means only interact within their own islands.
Structured concurrency relates to tasks and their children
- Structured tasks cannot leave any uncompleted child tasks before it finishes.
- All child tasks should be completed before the parent tasks completion in Structured tasks.
- Unstructured tasks can be live out of scope.
- In Unstructured tasks, child tasks no longer live than parent tasks. When the parent task is over all related children tasks will be over as well.
Example
In this example, withTaskGroup
is used to create a new task group. Inside the task group, a new child task is spawned to run the longRunningTask
function.
The for await
loop is used to wait for all child tasks to finish and collect their results.
The result of the task group is the sum of the results of all child tasks.
This is an example of structured concurrency because the parent task (the task group) cannot finish until all its child tasks have finished.
// Define a function that simulates a long-running task
func longRunningTask() async -> Int {
print("Starting long running task...")
await Task.sleep(2 * 1_000_000_000) // Sleep for 2 seconds
print("Long running task finished.")
return 42
}
// Define a function that uses structured concurrency to run the long-running task
func runTask() async {
let result = await withTaskGroup(of: Int.self) { group -> Int in
// Spawn a new child task
group.spawn {
await longRunningTask()
}
// Wait for all child tasks to finish and collect the results
var total = 0
for await result in group {
total += result
}
return total
}
print("Result: \(result)")
}
// Run the task
Task {
await runTask()
}
Example
// safe because whole class (including both property and method) is actor-isolated; `Task {…}` will be actor isolated, too
@MainActor
class SafeActorIsolatedClassExample {
var foo = Foo(bar: "baz")
func thisIsSafe() {
Task {
foo.bar = "qux"
}
}
}
// safe because both property and method are actor-isolated to same global actor; `Task {…}` will be actor isolated, too
class SafeActorIsolatedPropertyAndMethodExample {
@MainActor var foo = Foo(bar: "baz")
@MainActor func thisIsSafe() {
Task {
foo.bar = "qux"
}
}
}
// safe because `UIViewController` is actor-isolated to `@MainActor`,
// and therefore behaves like `SafeActorIsolatedClassExample`
// @available(iOS 2.0, *)
// @MainActor open class UIViewController :
class SafeViewController: UIViewController {
var foo = Foo(bar: "baz")
func thisIsSafe() {
Task {
foo.bar = "qux"
}
}
}
Sources:
Your Brain 🧠 on Swift Concurrency - iOS Conf SG 2023 (opens in a new tab) Mutating a mutable Struct within a Swift Task (opens in a new tab)