I have to use async/await
with withCheckedThrowingContinuation
to get a result from an external lib (I cannot modify this lib).
I call this from a UIViewController
(which means the Task
will be on the MainActor
if I understood correctly).
But the call to the external lib sometimes prints SWIFT TASK CONTINUATION MISUSE: leaked its continuation
, which means its completion will not be called if I understood correctly, and the code after this is not executed. It happens especially when I spam the button.
My first question is, why can I still use the main thread if the code after is never executed ? Shouldn’t the main thread be blocked there waiting for it’s completion ?
My second question is, how does the system know that the completion will never be called ? Does it track if the completion is retained in the external lib at runtime, if the owner of the reference dies, it raises this leak warning ?
My third question is, could this lead to a crash ? Or the system just cancel the Task and I shouldn’t worry about it ?
And my last question is, what can I do if I cannot modify the external lib ?
Here’s a sample of my code :
class MyViewController: UIViewController {
func onButtonTap() {
Task {
do {
try await callExternalLib() // <--- This can be called after the leak
}
}
}
func callExternalLib() async throws {
print("Main thread") // <--- This is always called on the main thread
try await withCheckedThrowingContinuation { continuation in
myExternalLib.doSomething {
continuation.resume()
}, { error in
continuation.resume(throwing: error)
}
}
print("Here thread is unlocked") // <--- This is never called when it leaks
}
}
2
Answers
That’s not how Swift Concurrency works.
The whole concept / idea behind Swift Concurrency is that the current thread is not blocked by an
await
call.To put it briefly and very simply, you can imagine an asynchronous operation as a piece of work that is processed internally on threads. However, while waiting for an operation, other operations can be executed because you’re just creating a suspension point. The whole management and processing of async operations is handled internally by Swift.
In general, with Swift Concurrency you should refrain from thinking in terms of “threads”, these are managed internally and the thread on which an operation is executed is deliberately not visible to the outside world.
In fact, with Swift Concurrency you are not even allowed to block threads without further ado, but that’s another topic.
If you want to learn more details about async/await and the concepts implemented by Swift, I recommend reading SE-0296 or watching one of the many WWDC videos Apple has published on this topic.
See the official documentation:
For the rest of your questions, I assume that you have shown us all the relevant parts of the code.
Only multiple calls to a continuation would lead to a crash (see my previous answer).
However, you should definitely make sure that the continuation is called, otherwise you will create a suspension point that will never be resolved. Think of it like an operation that is never completed and thus causes a leak.
According to the code you have shown us, there is actually only one possibility:
Calling
doSomething
multiple times causes calls to the same method that are still running to be canceled internally by the library and therefore the completion closures are never called.You should therefore check the documentation of
doSomething
to see what it says about multiple calls and cancelations.In terms of what you could do if the library doesn’t give you a way to detect cancelations:
Here is a very simple code example that should demonstrate how you can solve the problem for this case (2):
Note that this solution assumes that there are no other scenarios in which
doSomething
never calls its completion handlers.You said:
The library apparently does not handle concurrent requests.
This will happen if the library saves your completion-handler closures in properties, which are replaced with new closures when you call it concurrently. This is a mistake on the part of the library and it should either (a) not permit concurrent requests, immediately calling the error completion hander if one attempts to do so; or (b) be modified to support concurrent requests.
I would advise opening a ticket with he provider of that library. But until it is remedied, it is your responsibility to prevent concurrent requests. There are a variety of approaches:
The easiest way would be to disable the button:
If you quickly tap the button, say, five times and you really want the external library to run its routine five times, then you would want to make your tasks to run sequentially. The easiest way to do this would be an
AsyncChannel
from the Swift Async Algorithms package.So, you’d create a channel:
You would monitor that channel:
And then, on a button tap, you would
send
to your channel:So, a full MRE of this pattern would be:
The only interesting thing to note here is that you do not want to rely upon
deinit
to cancel your asynchronous sequence, but rather start the monitoring of the channel when the view appears and cancel it when the view is ultimately dismissed.The third approach would be to cancel the previous
doSomething
before submitting another. When wrapping the completion-handler-closure-based call in awithCheckedThrowingContinuation
, you’d also wrap that in awithTaskCancellationHandler
. E.g.,But, this pattern only works if the external library supports cancellation at all. I might wager that if they failed to handle something as basic as concurrent requests, they probably have not contemplated cancellation support, either. We simply cannot comment further without more details about this external library.
But, I include this for the sake of completeness.
To answer your follow-up questions:
No, an
await
does not block an actor. It suspends the current function’s execution, but frees the actor to carry on and do other work. Actors are “reentrant”. See Actor Reentrancy in SE-0306.When the last reference to the continuation is removed (i.e., the third party library releases those closures), the system verifies whether the continuation has been called or not. If not, you get this warning.
If you used the “unsafe” rendition,
withUnsafe[Throwing]Continuation
it would crash. It will not crash when using the “checked” rendition.But that does not mean you should not fix it. See SE-0300, which says:
Bottom line, you want to heed the warning and resolve the issue.