How to make an async Swift function @synchronized ?
You can have every Task
await the prior one. And you can use actor make sure that you are only running one at a time. The trick is, because of actor reentrancy, you have to put that "await prior Task
" logic in a synchronous method.
E.g., you can do:
actor Experiment {
private var previousTask: Task<Void, Error>?
func startSomethingAsynchronous() {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
Now I am using os_signpost
so I can watch this serial behavior from Xcode Instruments. Anyway, you could start three tasks like so:
import os.log
private let log = OSLog(subsystem: "Experiment", category: .pointsOfInterest)
class ViewController: NSViewController {
let experiment = Experiment()
func startExperiment() {
for _ in 0 ..< 3 {
Task { await experiment.startSomethingAsynchronous() }
}
os_signpost(.event, log: log, name: "Done starting tasks")
}
...
}
And Instruments can visually demonstrate the sequential behavior (where the ⓢ
shows us where the submitting of all the tasks finished), but you can see the sequential execution of the tasks on the timeline:
I actually like to abstract this serial behavior into its own type:
actor SerialTasks<Success> {
private var previousTask: Task<Success, Error>?
func add(block: @Sendable @escaping () async throws -> Success) {
previousTask = Task { [previousTask] in
let _ = await previousTask?.result
return try await block()
}
}
}
And then the asynchronous function for which you need this serial behavior would use the above, e.g.:
class Experiment {
let serialTasks = SerialTasks<Void>()
func startSomethingAsynchronous() async {
await serialTasks.add {
try await self.doSomethingAsynchronous()
}
}
private func doSomethingAsynchronous() async throws {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Task", signpostID: id, "Start")
try await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
os_signpost(.end, log: log, name: "Task", signpostID: id, "End")
}
}
How to wait for a bunch of async calls to finish to return a result?
We use task group to perform the requests in parallel and then await
the whole group.
The trick, though, is that they will not finish in the same order that we started them. So, if you want to preserve the order of the results, we can return every result as a tuple of the input (the URL) and the output (the string). We then collect the group result into a dictionary, and the map the results back to the original order:
static func save(files: [URL]) async throws -> [String] {
try await withThrowingTaskGroup(of: (URL, String).self) { group in
for file in files {
group.addTask { (file, try await save(file)) }
}
let dictionary = try await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
return files.compactMap { dictionary[$0] }
}
}
There are other techniques to preserve the order of the results, but hopefully this illustrates the basic idea.
Swift - Async/Await not stopping to let task finish
If I understood the problem, you see "Done" being printed before the getToken()
method is completed, right?
The problem is that print("Done")
is outside of the Task
.
When you call Task
, it starts running what's in the closure and it immediately resumes after the closure, while your task is running in the background.
Place your print()
inside the Task
closure, right after the getToken()
method, and you'll see that it'll be "Done" after you complete your POST request.
Task{
let accessToken = await getToken()
print("Done")
}
`Task` blocks main thread when calling async function inside
Consider:
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
You have marked populate
with async
, but there is nothing asynchronous here, and it will block the calling thread.
Then consider:
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}
That looks like it must be asynchronous, since it is launching a Task
. But this Task
runs on the same actor, and because the task is slow and synchronous, it will block the current executor.
If you do not want it to run on the main actor, you can either:
You can leave the view model on the main actor, but manually move the slow synchronous process to a detached task:
@MainActor
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() {
Task.detached {
var items = [String]()
for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
items.append("\(i)")
}
await MainActor.run { [items] in
self.items = items
}
}
}
}For the sake of completeness, you could also make the view model to be its own actor, and only designate the relevant observed properties as being on the main actor:
actor ViewModel: ObservableObject {
@MainActor
@Published var items = [String]()
func populate() {
Task {
var items = [String]()
for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
items.append("\(i)")
}
await MainActor.run { [items] in
self.items = items
}
}
}
}
I would generally lean towards the former, but both approaches work.
Either way, it will allow the slow process to not block the main thread. Here I tapped on the button twice:
See WWDC 2021 videos Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app, all of which are useful when trying to grok the transition from GCD to Swift concurrency.
Related Topics
Swiftui MACos Xcode Style Toolbar
Convert String to Cgfloat in Swift
How to Update Swift Dependencies in Xcode
Seeking an "Exit" Equivalent in Swift
Add Placeholder to Uitextfield, How to Set the Placeholder Text Programmatically in Swift
How to Color/Customize the Uiimagepickercontroller's Navigation Bar
Create a Timer Publisher Using Swift Combine
How Get Rootviewcontroller with iPados Multi Window (Scenedelegate)
"Missing Required Entitlement" for Nfctagreadersession
Check Availability in Switch Statement
Pop to Root View Using Tab Bar in Swiftui
Xcode 11 Doesn't Recognize Core Data Entity
How to Automatically Reflect Coredata+Icloud Changes in Swiftui View
Swift: How to Disable User Interaction While Touch Action Is Being Carried Out
Adding Nscoding as an Extension