I am learning async/await
and Task. So i learned that Task basically inherit actor.
Imagine I have a model:
class SomeModel: ObservableObject {
@Published var downloads: [Int] = []
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
downloads.append(data) // after request
}
}
And I have a simple View
struct DownloadView: View {
@EnvironmentObject var model: SomeModel
var body: some View {
List {
// USE model.downloads
}
.task {
do {
try await model.doSome()
} catch {}
}
}
I get an error that I am updating UI from background thread. Thats ok.
Then i add
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
await MainActor.run {
downloads.append(data)
} // after request
}
And I have still Error. System does not know that first append is Always main actor? Or this is a security from dummies if some on pus some suspended code before first append?
How I understand hierarchical await is needed for System know what tree of sub jobs need to suspend. But a suspension point is after first append, so it could not pass on different actor.
Code for reproduce:
Model
struct DownloadFile: Identifiable {
var id: String { return name }
let name: String
let size: Int
let date: Date
static let mockFiles = [DownloadFile(name: "File1", size: 100, date: Date()),
DownloadFile(name: "File2", size: 100, date: Date()),
DownloadFile(name: "File3", size: 100, date: Date()),
DownloadFile(name: "File4", size: 100, date: Date()),
DownloadFile(name: "File5", size: 100, date: Date()),
DownloadFile(name: "FIle6", size: 100, date: Date())]
static let empty = DownloadFile(name: "", size: 0, date: Date())
}
FirstView
struct ContentView: View {
@State var files: [DownloadFile] = []
let model: ViewModel
@State var selected = DownloadFile.empty {
didSet {
isDisplayingDownload = true
}
}
@State var isDisplayingDownload = false
var body: some View {
NavigationStack {
VStack {
// The list of files available for download.
List {
Section(content: {
if files.isEmpty {
ProgressView().padding()
}
ForEach(files) { file in
Button(action: {
selected = file
}, label: {
Text(file.name)
})
}
})
}
.listStyle(.insetGrouped)
}
.task {
try? await Task.sleep(for: .seconds(1))
files = DownloadFile.mockFiles
}
.navigationDestination(isPresented: $isDisplayingDownload) {
DownloadView(file: selected).environmentObject(model)
}
}
}
}
Second view
struct DownloadView: View {
let file: DownloadFile
@EnvironmentObject var model: ViewModel
@State var result: String = ""
var body: some View {
VStack {
Text(file.name)
if model.downloads.isEmpty {
Button {
Task {
result = try await model.download(file: file)
}
} label: {
Text("-- Download --")
}
}
Text(result)
}
}
}
viewModel
class ViewModel: ObservableObject {
@Published var downloads: [String] = []
func download(file: DownloadFile) async throws -> String {
downloads.append(file.name)
try await Task.sleep(for: .seconds(2))
return "Download Finished"
}
}
12
System does not know that first append is Always main actor?
How should the “system” (or more precisely the compiler) know this at this point?
There is no magic in Swift Concurrency that would recognize that if you access a property in one code location from a specific actor, all accesses to that property should be protected with that actor.
In your case, SomeModel
is not bound to a specific actor, nor is the doSome
function.
The task modifier creates a new top level task and if you don’t bind it to a specific actor you have the described issue.
Child tasks inherit the actor of the top-level task to which they belong, but for top-level tasks you must specifically specify an actor.
So what you actually should do is bind your view model to the main actor:
@MainActor
final class SomeModel: ObservableObject {
// ...
}
This means that access to all properties is protected by the MainActor
and any attempt to access a property from another actor must take place as an async
call. This is then enforced by the compiler and makes it clear that a special protected call must take place at this point.
Roughly speaking, it is as if you have to wait for a lock at this point, with the difference that the calling thread is not blocked as a result.
Or, if you are sure of what you are doing, just bind the downloads
property and maybe the doSome
method to the MainActor
and make sure that all other accesses to SomeModel
are protected accordingly:
final class SomeModel: ObservableObject {
@MainActor @Published private(set) var downloads: [Int] = []
// ...
@MainActor
func doSome() async throw {
// ...
}
}
At first glance, your implementation listed below has fairly clear concurrency issues:
func doSome() async throw {
// MAKE URL
downloads.append(1)
try await /// MAKE REQUEST
await MainActor.run {
downloads.append(data)
} // after request
}
Since the class of the method is not bound to any actor, the asynchronous doSome
function can be called by practically any task (and therefore thread).
This means that the method can also be called several times concurrently and in the first line you already make a concurrent access to downloads
, which is a problem because nothing protects downloads
at this point.
And while your second access to downloads
is made on the MainActor
, other tasks may still make another concurrent call to doSome
and access downloads
in the first line of this method.
I would generally recommend activating the “strict concurrency checking” option, which will detect a lot of concurrency issues at compile time and emit errors.
See here and here.
This mode can also be activated with Swift 5, but does not show as many possible errors as in Swift 6 mode.
11