Search code examples
swiftasync-awaittaskgrand-central-dispatchurlsession

One method in many Tasks async/await


Hi I have a case where I need to call the same method in multiple Tasks. I want to have a possibility to call this method one by one (sync) not in parallel mode. It looks like that:

var isReadyToRefresh: Bool = true

func refresh(value: Int) async {
    try! await Task.sleep(nanoseconds: 100_000_000) // imitation API CALL
    isReadyToRefresh = false
    print("Try to refresh: \(value)")
}

func mockCallAPI(value: Int) async {
    if isReadyToRefresh {
        await refresh(value: value)
    }
}

Task {
     await mockCallAPI(value: 1)
}

Task {
     await mockCallAPI(value: 2)
}

output:

Try to refresh: 1

Try to refresh: 2

my required output:

Try to refresh: 1 OR Try to refresh 2. Depends which task has been called as first one.

Any ideas?


Solution

  • You said:

    I want [the second attempt] to wait for the first refresh API finish

    You can save a reference to your Task and, if found, await it. If not found, then start the task. (And because we are using unstructured concurrency, remember to wrap it in a withTaskCancellationHandler.)

    Plus, I personally would move the logic about what to await/cancel into the “refresh” process, not the API call code. Thus:

    actor Refresh {
        var priorTask: Task<Void, Error>?
    
        func refresh(value: Int) async throws {
            if let priorTask {
                _ = try await priorTask.value
                return
            }
    
            let task = Task {
                try await mockCallAPI(value: value)
            }
    
            priorTask = task
    
            try await withTaskCancellationHandler {
                _ = try await task.value
                priorTask = nil
            } onCancel: {
                task.cancel()
            }
        }
    
        private func mockCallAPI(value: Int) async throws {
            try await Task.sleep(for: .seconds(0.1))        // imitation API CALL
            print("Try to refresh: \(value)")
        }
    }
    

    Apple showed an example of this pattern in the code associated with WWDC 2021 video, Protect mutable state with Swift actors.

    Their example is more complicated (a pattern to avoid duplicate network requests from being initiated by some image cache/downloader), but the kernel of the idea is the same: Save and await the Task.


    Note the above is designed around original question, to return the first request’s result, and avoid starting a subsequent request while the prior one is underway. This pattern is common in the cached result pattern where you might have duplicative requests all returning the exact same result (e.g., some static resource from a CDN, such as in the Apple example).

    But when we talk about a “refresh” process, the user often wants the latest results. With “refresh”, we often do not want to show the user the older, possibly out-of-date, results from a prior request. So, when refreshing, we generally want to cancel the prior request and start a new one:

    actor Refresh {
        var priorTask: Task<Void, Error>?
    
        func refresh(value: Int) async throws {
            let task = Task { [priorTask] in
                priorTask?.cancel()
                try await mockCallAPI(value: value)
            }
    
            priorTask = task
    
            try await withTaskCancellationHandler {
                _ = try await task.value
            } onCancel: {
                task.cancel()
            }
        }
    
        private func mockCallAPI(value: Int) async throws {…}
    }
    

    It is a subtle point, but note that we want to capture the priorTask to avoid races between multiple calls to this refresh process.

    So, you have these two options: Either (a) await/return the first request and avoid duplicative requests; or (b) cancel the prior request and initiate a new one, ensuring that the refresh returns the most current results. Generally, we would favor the first pattern when results are static, and the latter when results may change over time. The use of the term “refresh” often implies that we want the most current results, but it is completely up to you. I just wanted to show both patterns.