How to resume a continuation ensuring that the result is delivered on the MainActor? - swift

I have a continuation:
func a() async -> Int {
await withCheckedContinuation { continuation in
continuation.resume(returning: 3)
}
}
I would like all callers of this function to receive the result on the MainActor. I wouldn't like the caller to have to explicitly specify this rescheduling. I don't want this:
func c() async {
let three = await a()
await MainActor.run {
b(three)
}
}
What I instead want is for the entire code after returning to be performed on the MainThread until the next suspension point, something like this:
func c1() async {
let three = await a()
b(three) // Guaranteed main thread, although nothing speaks of it here
}
In a way, I want a to declare that I return only on main actor!, like this:
func a() #MainActor async -> Int {
await withCheckedContinuation { continuation in
continuation.resume(returning: 3)
}
}
Is there any way to even do this?
UPDATE:
Both commenters have suggested that I annotate the enclosing functions c and c1 with #MainActor.
#MainActor
func c() async {
let three = await a()
await MainActor.run {
b(three)
}
}
This doesn't do it like I need it. It says:
every time I await somebody, they must return on the main thread
But what I need instead is this:
every time somebody awaits me, they must get my result on the main thread

No, there is no way to do this.
If you await some function, you can decide on which thread will it return.
But being an await-able function, you can not make sure that your result will be delivered to the caller on a particular and/or main thread.

Related

Swift: Have a timeout for async/await function

Is there a way to attach a timeout to stop an async function if it takes too long?
Here's a simplified version of my code:
func search() async -> ResultEnum {
// Call multiple Apis one after the other
}
Task.init {
let searchResult = await search()
switch searchResult {
// Handle all result scenarios
}
}
I would like to have a deadline for the search() async function to provide a result, otherwise it should terminate and return ResultEnum.timeout.
Thank you Rob for your comments, and for the link you provided.
I had to make some changes though, For some reason the initial task fetchTask kept going even after cancellation, until I added Task.checkCancellation() to it.
Here's what the code looks like now, if anyone is facing a similar issue:
func search() async throws -> ResultEnum {
// This is the existing method as per my initial question.
// It calls multiple Apis one after the other, then returns a result.
}
// Added the below method to introduce a deadline for search()
func search(withTimeoutSecs: Int) async {
let fetchTask = Task {
let taskResult = try await search()
try Task.checkCancellation()
// without the above line, search() kept going until server responded long after deadline.
return taskResult
}
let timeoutTask = Task {
try await Task.sleep(nanoseconds: UInt64(withTimeoutSecs) * NSEC_PER_SEC)
fetchTask.cancel()
}
do {
let result = try await fetchTask.value
timeoutTask.cancel()
return result
} catch {
return ResultEnum.failed(NetworkError.timeout)
}
}
// Call site: Using the function (withTimeout:) instead of ()
Task.init {
let searchResult = await search(withTimeoutSecs: 6)
switch searchResult {
// Handle all result scenarios
}
}

Run function after two functions has ran

So let's say I have these three functions:
func func_1() {
Task { #MainActor in
let state = try await api.get1State(v!)
print("cState func_1: \(state!)")
}
}
func func_2() {
Task { #MainActor in
let state = try await api.get2State(v!)
print("cState func_2: \(state!)")
}
}
func func_3() {
Task { #MainActor in
let state = try await api.get3State(v!)
print("cState func_3: \(state!)")
}
}
Since these function get info from api, it might take a few seconds.
How can I run func_3, after both func_1 and func_2 is done running?
I would advise avoiding Task (which opts out of structured concurrency, and loses all the benefits that entails) unless you absolutely have to. E.g., I generally try to limit Task to those cases where I are going from a non-asynchronous context to an asynchronous one. Where possible, I try to stay within structured concurrency.
As The Swift Programming Language: Concurrency says:
Tasks are arranged in a hierarchy. Each task in a task group has the same parent task, and each task can have child tasks. Because of the explicit relationship between tasks and task groups, this approach is called structured concurrency. Although you take on some of the responsibility for correctness, the explicit parent-child relationships between tasks lets Swift handle some behaviors like propagating cancellation for you, and lets Swift detect some errors at compile time.
And I would avoid creating functions (func_1, func_2, and func_3) that fetch a value and throw it away. You would presumably return the values.
If func_1 and func_2 return different types, you could use async let. E.g., if you're not running func_3 until the first two are done, perhaps it uses those values as inputs:
func runAll() async throws {
async let foo = try await func_1()
async let bar = try await func_2()
let baz = try await func_3(foo: foo, bar: bar)
}
func func_1() async throws -> Foo {
let foo = try await api.get1State(v!)
print("cState func_1: \(foo)")
return foo
}
func func_2() async throws -> Bar {
let bar = try await api.get2State(v!)
print("cState func_2: \(bar)")
return bar
}
func func_3(foo: Foo, bar: Bar) async throws -> Baz {
let baz = try await api.get3State(foo, bar)
print("cState func_3: \(baz)")
return baz
}
Representing that visually using “Points of Interest” tool in Instruments:
The other pattern, if func_1 and func_2 return the same type, is to use a task group:
func runAll() async throws {
let results = try await withThrowingTaskGroup(of: Foo.self) { group in
group.addTask { try await func_1() }
group.addTask { try await func_2() }
return try await group.reduce(into: [Foo]()) { $0.append($1) } // note, this will be in the order that they complete; we often use a dictionary instead
}
let baz = try await func_3(results)
}
func func_1() async throws -> Foo { ... }
func func_2() async throws -> Foo { ... }
func func_3(_ values: [Foo]) async throws -> Baz { ... }
There are lots of permutations of the pattern, so do not get lost in the details here. The basic idea is that (a) you want to stay within structured concurrency; and (b) use async let or TaskGroup for those tasks you want to run in parallel.
I hate to mention it, but for the sake of completeness, you can used Task and unstructured concurrency. From the same document I referenced above:
Unstructured Concurrency
In addition to the structured approaches to concurrency described in the previous sections, Swift also supports unstructured concurrency. Unlike tasks that are part of a task group, an unstructured task doesn’t have a parent task. You have complete flexibility to manage unstructured tasks in whatever way your program needs, but you’re also completely responsible for their correctness.
I would avoid this because you need to handle/capture the errors manually and is somewhat brittle, but you can return the Task objects, and await their respective result:
func func_1() -> Task<(), Error> {
Task { #MainActor [v] in
let state = try await api.get1State(v!)
print("cState func_1: \(state)")
}
}
func func_2() -> Task<(), Error> {
Task { #MainActor [v] in
let state = try await api.get2State(v!)
print("cState func_2: \(state)")
}
}
func func_3() -> Task<(), Error> {
Task { #MainActor [v] in
let state = try await api.get3State(v!)
print("cState func_3: \(state)")
}
}
func runAll() async throws {
let task1 = func_1()
let task2 = func_2()
let _ = await task1.result
let _ = await task2.result
let _ = await func_3().result
}
Note, I did not just await func_1().result directly, because you want the first two tasks to run concurrently. So launch those two tasks, save the Task objects, and then await their respective result before launching the third task.
But, again, your future self will probably thank you if you remain within the realm of structured concurrency.

Unit testing a class that depends on an async function

I have a view model with a state property enum that has 3 cases.
protocol ServiceType {
func doSomething() async
}
#MainActor
final class ViewModel {
enum State {
case notLoaded
case loading
case loaded
}
private let service: ServiceType
var state: State = .notLoaded
init(service: ServiceType) {
self.service = service
}
func load() async {
state = .loading
await service.doSomething()
state = .loaded
}
}
I want to write a unit test that asserts that after load is called but before the async function returns, state == .loading .
If I was using completion handlers, I could create a spy that implements ServiceType, captures that completion handler but doesn't call it. If I was using combine I could use a schedular to control execution.
Is there an equivalent solution when using Swift's new concurrency model?
As you're injecting the depencency via a protocol, you're in a very good position for providing a Fake for that protocol, a fake which you have full control from the unit tests:
class ServiceFake: ServiceType {
var doSomethingReply: (CheckedContinuation<Void, Error>) -> Void = { _ in }
func doSomething() async {
// this creates a continuation, but does nothing with it
// as it waits for the owners to instruct how to proceed
await withCheckedContinuation { doSomethingReply($0) }
}
}
With the above in place, your unit tests are in full control: they know when/if doSomething was called, and can instruct how the function should respond.
final class ViewModelTests: XCTestCase {
func test_viewModelIsLoadingWhileDoSomethingSuspends() {
let serviceFake = ServiceFake()
let viewModel = ViewModel(service: serviceFake)
XCTAssertEquals(viewModel.state, .notLoaded)
let expectation = XCTestExpectation(description: "doSomething() was called")
// just fulfilling the expectation, because we ignore the continuation
// the execution of `load()` will not pass the `doSomething()` call
serviceFake.doSomethingReply = { _ in
expectation.fulfill()
}
Task {
viewModel.load()
}
wait(for: [expectation], timeout: 0.1)
XCTAssertEqual(viewModel.state, .loading)
}
}
The above test makes sure doSomething() is called, as you likely don't want to validate the view model state until you're sure the execution of load() reached the expected place - afterall, load() is called on a different thread, so we need an expectation to make sure the test properly waits until the thread execution reaches the expected point.
The above technique is very similar to a mock/stub, where the implementation is replaced with a unit-test provided one. You could even go further, and just have an async closure instead of a continuation-based one:
class ServiceFake: ServiceType {
var doSomethingReply: () async -> Void = { }
func doSomething() async {
doSomethingReply()
}
}
, and while this would give even greater control in the unit tests, it also pushes the burden of creating the continuations on those unit tests.
You can handle this the similar way you were handling for completion handler, you have the choice to either delay the completion of doSomething using Task.sleep(nanoseconds:) or you can use continuation to block the execution forever by not resuming it same as you are doing with completion handler.
So your mock ServiceType for the delay test scenario looks like:
struct HangingSevice: ServiceType {
func doSomething() async {
let seconds: UInt64 = 1 // Delay by seconds
try? await Task.sleep(nanoseconds: seconds * 1_000_000_000)
}
}
Or for the forever suspended scenario:
class HangingSevice: ServiceType {
private var continuation: CheckedContinuation<Void, Never>?
deinit {
continuation?.resume()
}
func doSomething() async {
let seconds: UInt64 = 1 // Delay by seconds
await withCheckedContinuation { continuation in
self.continuation?.resume()
self.continuation = continuation
}
}
}

What is the correct way to await the completion of two Tasks in Swift 5.5 in a function that does not support concurrency?

I have an app that does some processing given a string, this is done in 2 Tasks. During this time i'm displaying an animation. When these Tasks complete i need to hide the animation. The below code works, but is not very nice to look at. I believe there is a better way to do this?
let firTask = Task {
/* Slow-running code */
}
let airportTask = Task {
/* Even more slow-running code */
}
Task {
_ = await firTask.result
_ = await airportTask.result
self.isVerifyingRoute = false
}
Isn't the real problem that this is a misuse of Task? A Task, as you've discovered, is not really of itself a thing you can await. If the goal is to run slow code in the background, use an actor. Then you can cleanly call an actor method with await.
let myActor = MyActor()
await myActor.doFirStuff()
await myActor.doAirportStuff()
self.isVerifyingRoute = false
However, we also need to make sure we're on the main thread when we talk to self — something that your code omits to do. Here's an example:
actor MyActor {
func doFirStuff() async {
print("starting", #function)
await Task.sleep(2 * 1_000_000_000)
print("finished", #function)
}
func doAirportStuff() async {
print("starting", #function)
await Task.sleep(2 * 1_000_000_000)
print("finished", #function)
}
}
func test() {
let myActor = MyActor()
Task {
await myActor.doFirStuff()
await myActor.doAirportStuff()
Task { #MainActor in
self.isVerifyingRoute = false
}
}
}
Everything happens in the right mode: the time-consuming stuff happens on background threads, and the call to self happens on the main thread. A cleaner-looking way to take care of the main thread call, in my opinion, would be to have a #MainActor method:
func test() {
let myActor = MyActor()
Task {
await myActor.doFirStuff()
await myActor.doAirportStuff()
self.finish()
}
}
#MainActor func finish() {
self.isVerifyingRoute = false
}
I regard that as elegant and clear.
I would make the tasks discardable with an extension. Perhaps something like this:
extension Task {
#discardableResult
func finish() async -> Result<Success, Failure> {
await self.result
}
}
Then you could change your loading task to:
Task {
defer { self.isVerifyingRoute = false }
await firTask.finish()
await airportTask.finish()
}

'async' call in a function that does not support concurrency

I'm trying to use async/await with Swift 5.5. I have my async function, but whenever I try to call it, I get this error:
'async' call in a function that does not support concurrency
Here's the code sample:
class TryThis {
func getSomethingLater(_ number: Double) async -> String {
// test - sleep for 3 seconds, then return
Thread.sleep(forTimeInterval: 3)
return String(format: ">>>%8.2f<<<", number)
}
}
let tryThis = TryThis()
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
What's the solution for this??
The answer here is that the "await getSomethingLater" must be called from an async context. Literally that means changing this:
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
into this:
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
So the whole thing becomes:
class TryThis {
func getSomethingLater(_ number: Double) async -> String {
// test - sleep for 3 seconds, then return
Thread.sleep(forTimeInterval: 3)
return String(format: ">>>%8.2f<<<", number)
}
}
let tryThis = TryThis()
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
Here's the output:
result: >>> 3.14<<<
There's great info in the Meet async/await in Swift video from WWDC21.
When calling async code (including actor fields) from non-async code, you have to wrap it in a Task:
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
If you need it to escape that async context, you can use traditional callbacks:
func awaitedResult(callback: (String) -> Void) {
Task {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
callback(result)
}
}
awaitedResult { result in
print("result: \(result)")
}
Swift by Sundell also offers some alternatives involving Combine
Further reading: Concurrency — The Swift Programming Language (Swift 5.5)
The original error is produced because "top-level" await is not yet supported in Swift (that is, code at the global scope).
The error just means you need to provide the async function with an asynchronous context, like using Task, Task.detached or a TaskGroup, depending on the behaviour you want.
Task.detached {
let result = await tryThis.getSomethingLater(3.141592653589793238462)
print("result: \(result)")
}
However, your code snippet should work in the future when top-level await is eventually proposed, implemented and supported.
Top-level await support was mentioned as part of the original proposal for async/await SE-0296.
In my case I just refactored the:
async { some-code-which-can-take-some-time }
with:
Task { some-code-which-can-take-some-time }
This error occurs when you’ve tried to call an async function from a synchronous function, which is not allowed in Swift – asynchronous functions must be able to suspend themselves and their callers, and synchronous functions simply don’t know how to do that.
func doAsyncWork() async {
print("Doing async work")
}
func doRegularWork() {
Task {
await doAsyncWork()
}
}
doRegularWork()
Reference here