Reputation: 92
I have a question concerning asynchronous requests. I want to request data from different sources on the web. Each source might have the data I want but I do not know that beforehand. Because I only want that information once, I don't care about the other sources as soon as one source has given me the data I need. How would I go about doing that? I thought about doing it with a didSet and only setting it once, something like this:
var dogPicture : DogPicture? = nil {
didSet {
// Do something with the picture
}
}
func findPictureOfDog(_ sources) -> DogPicture? {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources)
However it would be very helpful, if I could just wait until findPictureOfDog()
is finished, because depending on if I find something or not, I have to ask the user for more input.
I don't know how I could do it in the above way, because if I don't find anything the didSet will never be called, but I should ask the user for a picture then.
A plus: isWhatIWanted()
is rather expensive, so If there was a way to abort the execution of the handler once I found a DogPicture would be great.
I hope I made myself clear and hope someone can help me out with this!
Best regards and thank you for your time
Upvotes: 2
Views: 218
Reputation: 437392
My original answer, below, predates async
-await
patterns of Swift concurrency. But I am going to insert this Swift concurrency rendition here, as that is ubiquitous nowadays, and the old completion-handler-based answer is a bit out of date.
But here is the Swift concurrency rendition:
func findPictureOfDog(_ sources: [String]) async -> DogPicture? {
…
}
In this case, where we would await
asynchronous functions inside findPictureOfDog
, we can make this an async
function, and simply await
its result.
For example, if you wanted to check all of these sources in parallel and show the first one that returned a result, it might look like:
func findPictureOfDog(_ sources: [String]) async -> DogPicture? {
await withTaskGroup(of: DogPicture?.self) { group in
for source in sources {
group.addTask { await pictureOfDog(for: source) }
}
for await result in group {
if let result {
group.cancelAll()
return result
}
}
return nil
}
}
func pictureOfDog(for source: String) async -> DogPicture? {
guard
let url = url(for: source),
let (data, response) = try? await URLSession.shared.data(from: url),
let httpResponse = response as? HTTPURLResponse,
(200 ..< 300) ~= httpResponse.statusCode
else { return nil }
// perhaps additional error checking?
return await data.getPicture()
}
The details here (on a question over four years old) are less material than the general observation that with async
-await
, you can return
a result from an async
method. But if working in an old code base without Swift concurrency, but rather completion handlers, then refer to my original answer, below.
A couple of things:
First, we’re dealing with legacy asynchronous processes (predating Swift concurrency), one wouldn’t return
the DogPicture
, but rather use completion-handler pattern. E.g. rather than:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
...
return dogPicture
}
You instead would probably do something like:
func findPictureOfDog(_ sources: [String], completion: @escaping (Result<DogPicture, Error>) -> Void) {
...
completion(.success(dogPicture))
}
And you’d call it like:
findPictureOfDog(sources: [String]) { result in
switch result {
case .success(let dogPicture): ...
case .failure(let error): ...
}
}
// but don’t try to access the DogPicture or Error here
While the above was addressing the “you can’t just return
value from asynchronous process”, the related observations is that you don’t want to rely on a property as the trigger to signal when the process is done. All of the “when first process finishes” logic should be in the findPictureOfDog
routine, and call the completion handler when it’s done.
I would advise against using properties and their observers for this process, because it begs questions about how one synchronizes access to ensure thread-safety, etc. Completion handlers are unambiguous and avoid these secondary issues.
You mention that isWhatIWanted
is computationally expensive. That has two implications:
If it is computationally expensive, then you likely don’t want to call that synchronously inside the dataTask(with:completionHandler:)
completion handler, because that is a serial queue. Whenever dealing with serial queues (whether main queue, network session serial queue, or any custom serial queue), you often want to get in and out as quickly as possible (so the queue is free to continue processing other tasks).
E.g. Let’s imagine that the Google request came in first, but, unbeknownst to you at this point, it doesn’t contain what you wanted, and the isWhatIWanted
is now slowly checking the result. And let’s imagine that in this intervening time, the Yahoo request that came in. If you call isWhatIWanted
synchronously, the result of the Yahoo request won’t be able to start checking its result until the Google request has failed because you’re doing synchronous calls on this serial queue.
I would suggest that you probably want to start checking results as they came in, not waiting for the others. To do this, you want a rendition of isWhatIWanted
the runs asynchronously with respect to the network serial queue.
Is the isWhatIWanted
a cancelable process? Ideally it would be, so if the Yahoo image succeeded, it could cancel the now-unnecessary Pinterest isWhatIWanted
. Canceling the network requests is easy enough, but more than likely, what we really want to cancel is this expensive isWhatIWanted
process. But we can’t comment on that without seeing what you’re doing there.
But, let’s imagine that you’re performing the object classification via VNCoreMLRequest
objects. You might therefore cancel
any pending requests as soon as you find your first match.
In your example, you list three sources. How many sources might there be? When dealing with problems like this, you often want to constrain the degree of concurrency. E.g. let’s say that in the production environment, you’d be querying a hundred different sources, you’d probably want to ensure that no more than, say, a half dozen running at any given time, because of the memory and CPU constraints.
All of this having been said, all of these considerations (asynchronous, cancelable, constrained concurrency) seem to be begging for an Operation
based solution.
So, in answer to your main question, the idea would be to write a routine that iterates through the sources, and calling the main completion handler upon the first success and make sure you prevent any subsequent/concurrent requests from calling the completion handler, too:
nil
your saved reference (so in case you have other requests that have completed at roughly the same time, that they can’t call the completion handler again, eliminating any race conditions); andNote, you’ll want to synchronize the the above logic, so you don’t have any races in this process of calling and resetting the completion handler.
Thus, that might look like:
func findPictureOfDog(_ sources: [String], completion: @escaping DogPictureCompletion) {
var firstCompletion: DogPictureCompletion? = completion
let synchronizationQueue: DispatchQueue = .main // note, we could have used any *serial* queue for this, but main queue is convenient
let completionOperation = BlockOperation {
synchronizationQueue.async {
// if firstCompletion not nil by the time we get here, that means none of them matched
firstCompletion?(.failure(DogPictureError.noneFound))
}
print("done")
}
for source in sources {
let url = URL(string: source)!
let operation = DogPictureOperation(url: url) { result in
if case .success(_) = result {
synchronizationQueue.async {
firstCompletion?(result)
firstCompletion = nil
Queues.shared.cancelAllOperations()
}
}
}
completionOperation.addDependency(operation)
Queues.shared.processingQueue.addOperation(operation)
}
OperationQueue.main.addOperation(completionOperation)
}
So what might that DogPictureOperation
might look like? I might create an asynchronous custom Operation
subclass (I just subclass a general purpose AsynchronousOperation
subclass, like the one here) that will initiate network request and then run an inference on the resulting image upon completion. And if canceled, it would cancel the network request and/or any pending inferences (pursuant to point 3, above).
Upvotes: 2
Reputation: 17844
You can use DispatchGroup
to run a check when all of your requests have returned:
func findPictureOfDog(_ sources: [String]) -> DogPicture? {
let group = DispatchGroup()
for source in sources {
group.enter()
let task = URLSession.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
dogPicture = data.getPicture()
}
group.leave()
}
task.resume()
}
group.notify(DispatchQueue.main) {
if dogPicture == nil {
// all requests came back but none had a result.
}
}
}
Upvotes: 0
Reputation: 285059
If you care about only one task use a completion handler, call completion(nil)
if no picture was found.
var dogPicture : DogPicture?
func findPictureOfDog(_ sources, completion: @escaping (DogPicture?) -> Void) {
for source in sources {
let task = URL.Session.shared.dataTask(with: source) { (data, response, error) in
// error handling ...
if data.isWhatIWanted() && dogPicture == nil {
let picture = data.getPicture()
completion(picture)
}
}
task.resume()
}
}
sources = ["yahoo.com", "google.com", "pinterest.com"]
findPictureOfDog(sources) { [weak self] picture in
if let picture = picture {
self?.dogPicture = picture
print("picture set")
} else {
print("No picture found")
}
}
Upvotes: 0