Structured Concurrency difference between addTask and addTaskUnlessCancelled Swift - ios

I am have confusion between Calling addTask() and addTaskUnlessCancelled. By definition addTask() on your group will unconditionally add a new task to the group
func testCancellation() async {
do {
try await withThrowingTaskGroup(of: Void.self) { group -> Void in
group.addTaskUnlessCancelled {
print("added")
try await Task.sleep(nanoseconds: 1_000_000_000)
throw ExampleError.badURL
}
group.addTaskUnlessCancelled {
print("added")
try await Task.sleep(nanoseconds: 2_000_000_000)
print("Task is cancelled: \(Task.isCancelled)")
}
group.addTaskUnlessCancelled {
print("added")
try await Task.sleep(nanoseconds: 5_000_000_000)
print("Task is cancelled: \(Task.isCancelled)")
}
group.cancelAll()
try await group.next()
}
} catch {
print("Error thrown: \(error.localizedDescription)")
}
}
If you want to avoid adding tasks to a cancelled group, we have to use the addTaskUnlessCancelled() method. But even with group.cancelAll(), it is adding all Task to group. Then what's the difference here and returning value which return true only if Task throws some error?

Short answer
The virtue of addTaskUnlessCancelled is that:
it will not even start the task if the group has been canceled; and
it allows you to add an early exit if the group has been canceled.
If you are interested, you can compare the source code for addTask with that of addTaskUnlessCancelled, right below it.
Long answer
I think the problem is best exemplified if we change the task group is processing requests over time (e.g. consuming an AsyncSequence).
Consider the following
func a() async {
await withTaskGroup(of: Void.self) { group in
for await i in tickSequence() {
group.addTask(operation: { await self.b() })
if i == 3 {
group.cancelAll()
}
}
}
}
func b() async {
let start = CACurrentMediaTime()
while CACurrentMediaTime() - start < 3 { }
}
func tickSequence() -> AsyncStream<Int> {
AsyncStream { continuation in
Task {
for i in 0 ..< 12 {
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
continuation.yield(i)
}
continuation.finish()
}
}
}
In this example, b is awaiting tick sequence events, calling c for each one. But we cancel the group after the fourth task is added. That results in the following:
So we can see the signpost ⓢ where the group was canceled after the fourth task was added, but it proceeded unaffected.
But that is because b is not responding to cancelation. So let's say you fix that:
func b() async {
let start = CACurrentMediaTime()
while CACurrentMediaTime() - start < 3 {
if Task.isCancelled { return }
}
}
You now see behavior like the following:
That is better, as b is now canceling, but a is still attempting to add tasks to the group even though it has been canceled. And you can see that b is repeatedly running (though at least it is now immediately terminating) and the task initiated by a is not finishing in a timely manner.
However, if you use group.addTaskUnlessCancelled it not only will not add new tasks to the group at all (i.e., it does not rely on the cancellation capabilities of the task), but it also lets you exit when the group is canceled.
func a() async {
await withTaskGroup(of: Void.self) { group in
for await i in tickSequence() {
guard group.addTaskUnlessCancelled(operation: { await self.b() })
else { break }
if i == 3 {
group.cancelAll()
}
}
}
}
That results in the desired behavior:
Obviously, this is a rather contrived example, explicitly constructed to illustrate the difference, but in many cases, the difference between addTask and addTaskUnlessCancelled is less stark. But hopefully the above illustrates the difference.
Bottom line, if you might be canceling groups in the middle of adding additional tasks to that group, addTaskUnlessCancelled is well advised. That having been said, if you do not cancel groups (e.g., canceling tasks is more common than canceling groups, IMHO), it is not entirely clear how often addTaskUnlessCancelled will really be needed.
Note, in the above, I removed all of the OSLog and signpost code to generate all of the above intervals and signposts in Xcode Instruments (as I did not want to distract from the question at hand). But, if you are interested, this was the actual code:
import os.log
private let log = OSLog(subsystem: "Test", category: .pointsOfInterest)
func a() async {
await withTaskGroup(of: Void.self) { group in
let id = log.begin(name: #function, "begin")
defer { log.end(name: #function, "end", id: id) }
for await i in tickSequence() {
guard group.addTaskUnlessCancelled(operation: { await self.b() }) else { break }
if i == 3 {
log.event(name: #function, "Cancel")
group.cancelAll()
}
}
}
}
func b() async {
let id = log.begin(name: #function, "begin")
defer { log.end(name: #function, "end", id: id) }
let start = CACurrentMediaTime()
while CACurrentMediaTime() - start < 3 {
if Task.isCancelled { return }
}
}
func tickSequence() -> AsyncStream<Int> {
AsyncStream { continuation in
Task {
for i in 0 ..< 12 {
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
continuation.yield(i)
}
continuation.finish()
}
}
}
With
extension OSLog {
func event(name: StaticString = "Points", _ string: String) {
os_signpost(.event, log: self, name: name, "%{public}#", string)
}
/// Manually begin an interval
func begin(name: StaticString = "Intervals", _ string: String) -> OSSignpostID {
let id = OSSignpostID(log: self)
os_signpost(.begin, log: self, name: name, signpostID: id, "%{public}#", string)
return id
}
/// Manually end an interval
func end(name: StaticString = "Intervals", _ string: String, id: OSSignpostID) {
os_signpost(.end, log: self, name: name, signpostID: id, "%{public}#", string)
}
func interval<T>(name: StaticString = "Intervals", _ string: String, block: () throws -> T) rethrows -> T {
let id = OSSignpostID(log: self)
os_signpost(.begin, log: self, name: name, signpostID: id, "%{public}#", string)
defer { os_signpost(.end, log: self, name: name, signpostID: id, "%{public}#", string) }
return try block()
}
}

Related

How to execute code *after* async function has finished and returned to caller's context?

I have an async function and I want to execute some task after the function has returned to the original context.
Here's a sample:
actor MyActor {
func doTask() await -> Int {
let result = await operation()
Task {
print("2. All tasks done!")
}
return result
}
}
let actor = MyActor()
let taskResult = await actor.doTask()
print("1. Task done")
I want output always to be
Task done
All tasks done!
However, sometimes the output is reversed.
I suppose this is because Swift might be switching threads when returning to the original caller.
Is there any way to actually wait for it so that "2. All tasks done" is called last?
I want to execute some task after the function has returned to the original context.
There's no magic here. If you want a task to execute at a certain point, then you need to start it at that point, not before.
What you are asking for is almost literally the same as
How do I make the print("2 ...") appear after the print("1 ...") in the following.
struct MyStruct {
func doTask() -> Int {
let result = operation()
print("2. All tasks done!")
return result
}
}
let actor = MyStruct()
let taskResult = actor.doTask()
print("1. Task done")
In your case, the only way to ensure the print("2 ...") appears after the print("1 ...") is to start the asynchronous task after you have done the print("1 ...").
You'll need to split the actor function in two.
actor MyActor {
func doOperation() async -> Int {
let result = await operation()
return result
}
func doTask(_ result: Int) async {
Task {
print("2. All tasks done!")
// Use result in some way if you need to
}
}
}
let actor = MyActor()
let taskResult = await actor.doOperation()
print("1. Task done")
await actor.doTask(taskResult)

EXC_BAD_ACCESS when initializing Dictionary of CurrentValueSubject in Swift

I am trying to create a class that executes data loading once and returns the data to all callers of the method while the data was loading to not perform the data loading for the same item (identifier) more than once. The issue I am having is that it seems to crash on the first initialization of CurrentValueSubject for an identifier. This only happens if the downloadStuff returns an Error I have no idea what's wrong. Here is a reproduction of the issue.
Class that does the synchronization:
class FetchSynchronizer<T, ItemIdentifier: Hashable> {
typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)
enum FetchCondition {
// executes data fetching only once
case executeFetchOnlyOnce
// re-executes fetch if request failed
case retryOnlyIfFailure
// always executes fetch even if response is cached
case noDataCache
// custom condition
case custom((CustomParams) -> Bool)
}
struct LoadingState<T> {
let result: Result<T, Error>
let isLoading: Bool
init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
self.result = result ?? .failure(NoResultsError())
self.isLoading = isLoading
}
}
private var cancellables = Set<AnyCancellable>()
private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
func startLoading(identifier: ItemIdentifier,
fetchCondition: FetchCondition = .executeFetchOnlyOnce,
loaderMethod: #escaping () async -> Result<T, Error>) async -> Result<T, Error> {
// initialize loading tracker for identifier on first execution
var isFirstExecution = false
if isLoading[identifier] == nil {
print("----0")
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
isFirstExecution = true
}
guard let currentIsLoading = isLoading[identifier] else {
assertionFailure("Should never be nil because it's set above")
return .failure(NoResultsError())
}
if currentIsLoading.value.isLoading {
// loading in progress, wait for finish and call pending callbacks
return await withCheckedContinuation { continuation in
currentIsLoading.filter { !$0.isLoading }.sink { currentIsLoading in
continuation.resume(returning: currentIsLoading.result)
}.store(in: &cancellables)
}
} else {
// no fetching in progress, check if it can be executed
let shouldFetchData: Bool
switch fetchCondition {
case .executeFetchOnlyOnce:
// first execution -> fetch data
shouldFetchData = isFirstExecution
case .retryOnlyIfFailure:
// no cached data -> fetch data
switch currentIsLoading.value.result {
case .success:
shouldFetchData = false
case .failure:
shouldFetchData = true
}
case .noDataCache:
// always fetch
shouldFetchData = true
case .custom(let completion):
shouldFetchData = completion((isFirstLoad: isFirstExecution,
result: currentIsLoading.value.result))
}
if shouldFetchData {
currentIsLoading.send(LoadingState(isLoading: true))
// fetch data
return await withCheckedContinuation { continuation in
Task {
// execute loader method
let result = await loaderMethod()
let state = LoadingState(result: result,
isLoading: false)
currentIsLoading.send(state)
continuation.resume(returning: result)
}
}
} else {
// use existing data
return currentIsLoading.value.result
}
}
}
}
Example usage:
class Executer {
let fetchSynchronizer = FetchSynchronizer<Data?, String>()
func downloadStuff() async -> Result<Data?, Error> {
await fetchSynchronizer.startLoading(identifier: "1") {
return await withCheckedContinuation { continuation in
sleep(UInt32.random(in: 1...3))
print("-------request")
continuation.resume(returning: .failure(NSError() as Error))
}
}
}
init() {
start()
}
func start() {
Task {
await downloadStuff()
print("-----3")
}
DispatchQueue.global(qos: .utility).async {
Task {
await self.downloadStuff()
print("-----2")
}
}
DispatchQueue.global(qos: .background).async {
Task {
await self.downloadStuff()
print("-----1")
}
}
}
}
Start the execution:
Executer()
Crashes at
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
Any guidance would be appreciated.
Swift Dictionary is not thread-safe.
You need to make sure it is being accessed from only one thread (i.e queue) or using locks.
EDIT - another solution suggested by #Bogdan the question writer is to make the class an actor class which the concurrency safety is taken care of by the compiler!
By dispatching to a global queue, you increase the chance that two threads will try and write into the dictionary “at the same time” which probably causes the crash
Take a look at these examples.
How to implement a Thread Safe HashTable (PhoneBook) Data Structure in Swift?
https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift

How to call multiple Api based on the previous Api success

I wanted to make multiple API call in the same screen, but when one api fails other api should not be called? The below code is working fine. but what I need is , how can I refactor this in a more simpler way?
ApplicationService.requestAppEndPointUrl { success, error in
if success {
ApplicationService.appLinkDownload { success, error in
if success{
ApplicationService.requestApplicationSession { success, error in
if success {
ApplicationService.validateSdk { success, error in
if success {
ApplicationService.requestApplicationDetails { success, error in
if success{
print("Success")
}
else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
}else{
self.showErrorAlert(error)
}
}
If ApplicationService is a class/struct that you can modify, you could convert the synchronous function calls with completion handler to asynchronous function calls, using Swift 5.5 concurrency. The code would look like:
do {
try await ApplicationService.requestAppEndPointUrl()
try await ApplicationService.appLinkDownload()
try await ApplicationService.requestApplicationSession()
try await ApplicationService.validateSdk()
try await ApplicationService.requestApplicationDetails()
} catch {
self.showErrorAlert(error)
}
Then, the 1st error would break the call chain, would be cought and shown.
If you had to use the synchronous ApplicationService functions, you could convert them to asynchronous functions by using async wrappers as shown in the link cited above.
for this, you need to use OperationQueue & ** DispatchGroup** where you can make your API calls in BlockOperation and one operation depends on another, and their dispatch group helps you to hold the API calls.
func apiCall() {
let dispatchGroup = DispatchGroup()
let queue = OperationQueue()
let operation1 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestAppEndPointUrl {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestAppEndPointUrl()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
let operation2 = BlockOperation {
dispatchGroup.enter()
ApplicationService.appLinkDownload {
print("ApplicationService.appLinkDownload()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation2.addDependency(operation1)
let operation3 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationSession {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationSession()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation3.addDependency(operation2)
let operation4 = BlockOperation {
dispatchGroup.enter()
ApplicationService.validateSdk {
print("ApplicationService.validateSdk()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation4.addDependency(operation3)
let operation5 = BlockOperation {
dispatchGroup.enter()
ApplicationService.requestApplicationDetails {
Thread.sleep(forTimeInterval: Double.random(in: 1...3))
print("ApplicationService.requestApplicationDetails()")
dispatchGroup.leave()
}
dispatchGroup.wait()
}
operation5.addDependency(operation4)
queue.addOperations([operation1, operation2, operation3, operation4, operation5], waitUntilFinished: true)
}
I know code is a little bit nasty.
and if you are using swift 5.5 then use Async await mentioned by #Reinhard Männer
Thats a pretty question. So if you don't want to pass data from one api call to the other the solution is pretty easy.
All you have to do is to make a Service Manager that will manage all the service calls.
You can make an enum based on the services you want to call
enum ServiceEnum {
case requestAppEndPointUrl
case appLinkDownload
case requestApplicationSession
case validateSdk
case requestApplicationDetails
}
enum ServiceState {
case notStarted
case inProgress
case finished
case failed
}
Then you can make a class of the service
class Service {
var service: ServiceEnum
var serviceState: ServiceState = .notStarted
init(service: ServiceEnum) {
self.service = service
}
func updateState(serviceState: ServiceState) {
self.serviceState = serviceState
}
}
After that you will need to make the Service Manager
class ServiceManager {
private var services = [Services]()
private var indexForService: Int {
didSet {
self.startServices()
}
}
init() {
services.append(.requestAppEndPointUrl)
services.append(.appLinkDownload)
services.append(.requestApplicationSession)
services.append(.validateSdk)
services.append(.requestApplicationDetails)
indexForService = 0
}
func startServices() {
// Check if the process has ended
guard indexForService < services.count else {
// If you want you can call an event here.
// All the services have been completed
return
}
// Check that we have services to call
guard services.count > 0 else {
return
}
handleService(service: services[indexForService])
}
func haveAllFinished() -> Bool {
return services.fist(where: { $0.serviceState == .failed || $0.serviceState == .notStarted || $0.serviceState == .inProgress }) == nil
}
func handleService(service: Service) -> Bool {
switch(service) {
case .requestAppEndPointUrl:
ApplicationService.requestAppEndPointUrl { success, error in
let serviceState = success ? .finished : .failed
service.updateState(serviceState: serviceState)
if success {
self.indexForService += 1
} else {
self.showErrorAlert(error)
}
}
// Do the same for other services
}
}
}

Swift/iOS - How to use a value from one scope/function and pass it into another?

I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.

iOS - SwiftUI - Navigate to the next screen AFTER async actions performed

I am pretty new to SwiftUI and with DispatchGroups and DispatchQueues.
I would like to create a Button which processes some server requests and then use the returned data with a CoreML model to predict some score. Once, the score is predicted, then the app can navigate to the next screen
Here is the sequence of actions which need to be done before moving to the next screen
// exemple of sequence of actions
let group = DispatchGroup()
group.enter()
DispatchQueue.main.async {
self.name = self.names[self.selectedCompanyIndex]
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex])
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex])
group.leave()
}
group.notify(queue: .main) {
print("done")
}
//function for fetching tweets
func fetchTweets(company: String) {
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
}
}
//function for making predictions via the coreML model
func makePrediction(with tweets: [TextClassifier1Input], type: String) {
do {
let predictions = try self.sentimentClassifier.predictions(inputs: tweets)
var sentimentScore = 0
for pred in predictions {
if pred.label == "pos" {
sentimentScore += 1
} else if pred.label == "neg" {
sentimentScore -= 1
} else {
print("something sent wrong: --> ", pred.label)
}
}
if type == "#" {
arobaseScore = sentimentScore
} else if type == "#" {
hashScore = sentimentScore
}
} catch {
print("There was an error with the ML model: --> ", error)
}
}
The problem is the navigation is executed on the button click whereas I want the previous actions to be done before.
Can anyone let me know how should I use DispatchGroups and DispatchQueue to run my code in the correct sequence
Thanks in advance for your help
welcome to Stack Overflow.
You are not waiting for swifter.searchTweet to complete before doing the update.
The first step is to add a completion handler to fetchTweets. The completion handler is a way of reporting when fetchTweets has finished the networking.
//function for fetching tweets
func fetchTweets(company: String, completion: #escaping () -> Void) {
// ^^^^^^^^^^ Added completion handler
swifter.searchTweet(
using: company,
lang: "en",
count: 100,
tweetMode: .extended,
success: { (results, metadata) in
var tweets = [TextClassifier1Input]()
for i in 0...99 {
if let tweet = results[i]["full_text"].string {
tweets.append(TextClassifier1Input(text: tweet))
}
}
let searchType = String(company.first!)
self.makePrediction(with: tweets, type: searchType)
completion() // <-- Call completion on success
}) { (error) in
print("There was an error with the Twitter API: --> ", error)
completion() // <-- Call completion on failure
}
}
After that is done, then you can enter and leave the group when the networking is done. The code in notify will then be called. It's in that code block where you need to update the UI.
let group = DispatchGroup()
self.name = self.names[self.selectedCompanyIndex]
group.enter()
self.fetchTweets(company: self.arobases[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.enter()
self.fetchTweets(company: self.hashes[self.selectedCompanyIndex]) {
group.leave() // <-- leave the group in the completion handler
}
group.notify(queue: .main) {
print("done")
// BEGIN UPDATING THE UI
// NOTE: Here is where you update the UI.
// END UPDATING THE UI
}
for those using programmatic navigation, trying to navigate before a call to the backend is done will cause all sorts of problems where the view wants to still stay until the call is over and your navigation logic wants to continue

Resources