Swift: GroupDispatch
Follow up to ParseSwift queries | by Michael Dacanay | Jun, 2023 | Medium
Synchronizing the execution of multiple asynchronous code blocks:
- Delete posts authored by user
- Delete user
- Logout user session
These 3 tasks needed to be executed in sequential order. For example, the logout invalidates the session token, meaning any operations related to current user account must be done beforehand. An obstacle I faced was a race condition where, due to asynchronous control flow, deleting posts or deleting the user would result in an invalid session token error because the logic that deleted the user (2) came after the (3) logout logic.
To guarantee the execution order, the DispatchGroup
methods: enter()
& leave()
were used. In addition, this example was more complex in that this consisted of 3+ steps, where a step had to be fully complete before the next began.
Here is the barebones skeleton:
let q1 = DispatchGroup()
let q2 = DispatchGroup()
q1.enter()
q2.enter()
DispatchQueue.global().sync {
// asynchronous code #1
q1.leave() // make sure to place inside the async code/function completion handler
}
// only runs when q1.leave() is called
q1.notify(queue: .main) {
// asynchronous code #2
q2.leave() // make sure to place inside the async code/function completion handler
}
// called when q2.leave() executes
q2.notify(queue: .main) {
// code #3
}
The tricky thing is where to place the DispatchGroup.leave()
method. Make sure to place it in a location where the async function/code has finished executing. Most of the time, this is the completion handler. Simply add a completion handler, by adding { ... }
after the function name.
Async code:
fetchData {
// start of completion handler
result in
switch result {
case .success(let data):
// Handle successful response and process the data
print("Data received:", data)
case .failure(let error):
// Handle error
print("Error occurred:", error)
}
// end of comletion handler, good place for .leave() method
}
In this example, fetchData
has completed and the return value is stored in data
and the status of the async function is in result
. Make sure not to place .leave()
method at end of switch result { ... }
block. If result == .success
, the commands in the case .success
block will execute, but not any other commands in result
block.
But why are parentheses not needed for the fetchData()
function completion handler?
Both performAsyncTask { ... }
and performAsyncTask() { ... }
are valid ways to call the function when the closure is the only parameter. The former form, without the parentheses, is more commonly used in Swift code and is often referred to as "trailing closure syntax."
Full code and stdout:
// Get posts that user has authored
let query = try? Post.query().where("user" == currentUser)
print("Current user id: ", currentUser.objectId!)
// Synchronize the execution of multiple tasks:
// 1. Delete posts 2. Delete user 3. Logout user session
let firstGroup = DispatchGroup()
let secondGroup = DispatchGroup()
firstGroup.enter()
secondGroup.enter() // lock the secondGroup.notify() block until secondGroup.leave() is called
// executing a task on a background queue
DispatchQueue.global(qos: .background).sync {
print("DispatchQueue.global().async start")
// Executes the query asynchronously
query?.find { result in
switch result {
case .success(let posts):
print("Successfully retrieved \(posts.count) posts.")
// Delete posts associated with the user
// self?.deletePosts(posts)
for post in posts {
firstGroup.enter()
print("in post for loop start")
post.delete { result in
switch result {
case .success:
// Post deletion successful
print("Post deleted successfully")
case .failure(let error):
// Handle the error that occurred during post deletion
print("Error deleting post: \(error)")
}
print("in post.delete")
firstGroup.leave()
}
print("in post for loop end")
}
case .failure(let error):
// Handle the error retrieving posts
print("Error retrieving posts: \(error)")
print("in query.find")
}
print("in query.find switch statement")
firstGroup.leave()
}
print("in DispatchQueue.global().async end")
}
print("at group.wait 1")
// group.wait()
firstGroup.notify(queue: .main) {
print("second block")
DispatchQueue.global().sync {
currentUser.delete { result in
switch result {
case .success:
// Account deletion successful
print("Account deleted successfully")
case .failure(let error):
// Handle the error that occurred during account deletion
print("Error deleting account: \(error)")
print("in switch statement 2")
}
// group.leave() needs to be inside the asynchrounous function. This means that the function has completed processing and returned a result (success/failure)
secondGroup.leave()
}
}
}
// logout invalidates the session token, any operations related to current user account must be done beforehand
secondGroup.notify(queue: .main) {
print("in logout block")
NotificationCenter.default.post(name: Notification.Name("logout"), object: nil)
print("logging out dispatched")
}
Console, output from print()
statements:
Current user id: XXXXXXXX
DispatchQueue.global().async start <--- first block
in DispatchQueue.global().async end
at group.wait 1 <--- out of first block??
Successfully retrieved 1 posts.
in post for loop start
in post for loop end
in query.find switch statement
Post deleted successfully
in post.delete
second block <--- second block
Account deleted successfully
in logout block <--- third block
logging out dispatched
Further reading:
- swift — Waiting until the task finishes — Stack Overflow
- Async await in Swift explained with code examples — SwiftLee (avanderlee.com) — using
Task.init
to support calling async method in sync context - Eliminate data races using Swift Concurrency — WWDC22 — Videos — Apple Developer
- Race condition | Swiftly Engineered iOS