1

I'm trying to fiddle with concurrent/serial queues and sync/async operations and came across a scenario which I'm not able to solve. Would be glad if someone can assist.

So it goes like this -

I have a queue and I'm trying to simulate an async image download operation by using asynAfter, and I'm able to get below result by this code.

var downloadQueue = DispatchQueue(label: "com.image.download", attributes: .concurrent)

var operation1 = {
    downloadQueue.asyncAfter(deadline: DispatchTime.now() + 6.0) {
        print("Image Download 1 Done")
    }
}

var operation2 = {
    downloadQueue.asyncAfter(deadline: DispatchTime.now() + 4.0) {
        print("Image Download 2 Done")
    }
}

var operation3 = {
    downloadQueue.asyncAfter(deadline: DispatchTime.now() + 2.0) {
        print("Image Download 3 Done")
    }
}

operation1()
operation2()
operation3()

OUTPUT:
Image Download 3 Done //Prints after 2 seconds
Image Download 2 Done //Prints after 4 seconds
Image Download 1 Done //Prints after 6 seconds

Now the question arises if I want to get below 2 scenarios -

  1. I want operation2 to start after my operation1 finishes, and operation3 to start after operation2 finishes. So that all operations are completed in combined (6.0+4.0+2.0) 12.0 seconds.
  2. I want all operations to start simultaneously, but completions to trigger in order they were entered in queue. So that all operations are completed in combined 6.0 seconds.

I tried serial queue and concurrent queue with sync/async blocks, but everytime answer is same. Please guide.

koen
  • 5,383
  • 7
  • 50
  • 89
sudhanshu-shishodia
  • 1,100
  • 1
  • 11
  • 25

3 Answers3

0

Suppose you have an array with imageURLs that contains all image URL that you want to download.

let imageUrls = ["imageUrl1","imageUrl2","imageUrl3"]

Here is the block method that will process operation ex: image download in this case.

func getResult(url:String, completion: @escaping (Any?) -> Void) {
   
   .... // THIS IS YOUR PROCESS THAT YOU WANT TO EXECUTE
   // After completing process you got the image 
    completion(image)
}

In the completion block, you just pass the value that you have got (image in this case)

Now, this is the main process to use the getResult block method. Suppose you have a method like downloadAllImages that need imageUrls.

func downloadAllImages(imageUrls: [String]) {
    var imageUrls = imageUrls
    if imageUrls.count > 0 {
         getResult(url: imageUrls[0]) { (data) in
            // Now you are in the Main thread 
            // here data is the output got the output 
           imageUrls.remove(at: 0)// remove the down element
           downloadAllImages(imageUrls: imageUrls) // Again call the next one 
        }
    }
}

Hope you understand.

Faysal Ahmed
  • 7,501
  • 5
  • 28
  • 50
0

The point of asyncAfter(deadline:) is to submit a block to the queue at some point in the future. But what you're saying is that you want to submit a block now and have it block the queue until it has completed. First, if you want things to occur in order, then you want a serial queue (and in fact you almost always want a serial queue).

let downloadQueue = DispatchQueue(label: "com.image.download")

Then you're saying you have a task that you want to take 6 seconds, and block the queue until it's done. That's generally not something you should do, but for testing it's of course fine, and you'd use sleep to achieve it. (You should almost never use sleep, but again, for testing it's fine.)

let operation1 = {
    Thread.sleep(forTimeInterval: 6)
    print("Image Download 1 Done")
}
let operation2 = {
    Thread.sleep(forTimeInterval: 4)
    print("Image Download 2 Done")
}
let operation3 = {
    Thread.sleep(forTimeInterval: 2)
    print("Image Download 3 Done")
}

And submit them:

downloadQueue.async(execute: operation1)
downloadQueue.async(execute: operation2)
downloadQueue.async(execute: operation3)

If, alternately, you want these to run in parallel, you can use a concurrent queue instead.

For non-testing situations, there are other techniques you generally should use (most commonly DispatchGroup), but for something where you're just simulating something taking time, this is fine.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
0

You said:

Now the question arises if I want to get below 2 scenarios -

  1. I want operation2 to start after my operation1 finishes, and operation3 to start after operation2 finishes. So that all operations are completed in combined (6.0+4.0+2.0) 12.0 seconds.

First, this is a pattern generally to be avoided with network requests because you’re going to magnify network latency effects. You would only use this pattern where absolutely needed, e.g. if request 1 was a “sign in” operation; or, you needed something returned in the first request in order to prepare the subsequent request).

Often we’d do something simple, such as initiating the subsequent request in the completion handler of the first. Or, if you wanted a more flexible set of dependencies from a series of requests, you adopt a pattern that doesn’t use dispatch queues, e.g. you might create a custom, asynchronous Operation subclass that only completes when the network request is done (see point 3 in https://stackoverflow.com/a/57247869/1271826). Or if targeting recent OS versions, you might use Combine. There are a whole bunch of alternatives here. But you can’t just start a bunch of asynchronous tasks and have them run sequentially without one of these sorts of patterns.

  1. I want all operations to start simultaneously, but completions to trigger in order they were entered in queue. So that all operations are completed in combined 6.0 seconds.

The whole idea of concurrent patterns is that you shouldn’t care about the order that they finish. So, use a structure that is not order-dependent, and then you can store the results as they come in. And use dispatch group to know when they’re all done.

But one thing at a time. First, how do you know when a bunch of concurrent requests are done? Dispatch groups. For example:

let group = DispatchGroup()

group.enter()
queue.asyncAfter(deadline: .now() + 6) {
    defer { group.leave() }
    print("Image Download 1 Done")
}

group.enter()
queue.asyncAfter(deadline: .now() + 4) {
    defer { group.leave() }
    print("Image Download 2 Done")
}

group.enter()
queue.asyncAfter(deadline: .now() + 2) {
    defer { group.leave() }
    print("Image Download 3 Done")
}

group.notify(queue: .main) {
    // all three are done
}

Now, how do you take these requests, store the results, and retrieve them in the original order when you’re all done? First, create some structure that is independent of the order of the tasks. For example, let’s say you were downloading a bunch of images from URLs, then create a dictionary.

var images: [URL: UIImage] = [:]

Now fire off the requests concurrently:

for url in urls {
    group.enter()
    downloadImage(url) { result in
        defer { group.leave() }
        
        // do something with the result, e.g. store it in our `images` dictionary
        
        switch result {
        case .failure(let error): print(error)
        case .success(let image): images[url] = image
        }
    }
}

// this will be called on main queue when they’re all done

group.notify(queue: .main) {
    // if you want to pull them in the original order, just iterate through your array
    
    for url in urls {
        if let image = images[url] {
            print("image \(url) has \(image.size)")
        }
    }
}

By the way, the above is using the following method to retrieve the images:

enum DownloadError: Error {
    case unknown(Data?, URLResponse?)
}

@discardableResult
func downloadImage(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) -> URLSessionTask {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in 
        guard 
            let responseData = data,
            let image = UIImage(data: responseData),
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            error == nil 
        else {
            DispatchQueue.main.async {
                completion(.failure(error ?? DownloadError.unknown(data, response)))
            }
            return
        }
        
        DispatchQueue.main.async {
            completion(.success(image))
        }
    }

    task.resume()

    return task
}

The details here are not relevant. The key observation is that you should embrace concurrent patterns for determining when tasks are done (using dispatch groups, for example) and retrieving the results (store results in an unordered structure and access them in a manner that honors the order you intended).

Rob
  • 415,655
  • 72
  • 787
  • 1,044