1

Suppose you have an array, and you want to iterate over each element in the array and call a function obj.f which accepts that element as a parameter.

f is asynchronous, and completes nearly instantly, but it invokes a callback handler found in obj.

What would be the best way to match each element only after the previous finishes?

Here is one way:

let arr = ...

var arrayIndex = 0
var obj: SomeObj! // Required

obj = SomeObj(handler: {
    ...
    arrayIndex += 1
    if arrayIndex < arr.count {
        obj.f(arr[arrayIndex])
    }
})
obj.f(arr[0]) // Assumes array has at least 1 element

This works fine, but isn't ideal.

I could use a DispatchSemaphore, but that isn't great because it blocks the current thread.

Also, the reason why each operation must run only when the previous has finished is because the api I'm using requires it (or it breaks)

I was wondering if there was a better/more elegant way to accomplish this?

swifter
  • 23
  • 3
  • This is quite abstract, and probably an XY problem. Could you please explain exactly *what* you’re trying to achieve, not *how* you’re trying to achieve it? – Alexander Aug 27 '21 at 03:17
  • "What would be the best way to match each element only after the previous finishes?" What makes one way the best of all? – El Tomato Aug 27 '21 at 03:18
  • @Alexander One example could be using `AVAssetExportSession` on an array of images, and not wanting to export too many at once – swifter Aug 27 '21 at 03:44

2 Answers2

1

You say:

Suppose you have an array, and you want to iterate over each element in the array and call a function ... which accepts that element as a parameter.

The basic GCD pattern to know when a series of asynchronous tasks are done is the dispatch group:

let group = DispatchGroup()

for item in array {
    group.enter()

    someAsynchronousMethod { result in
        // do something with `result`

        group.leave()
    }
}

group.notify(queue: .main) {
    // what to do when everything is done
}

// note, don't use the results here, because the above all runs asynchronously; 
// return your results in the above `notify` block (e.g. perhaps an escaping closure).

If you wanted to constrain this to, say, a max concurrency of 4, you could use the non-zero semaphore pattern (but make sure you don't do this from the main thread), e.g.

let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 4)

DispatchQueue.global().async {
    for item in array {
        group.enter()
        semaphore.wait()
    
        someAsynchronousMethod { result in
            // do something with `result`
    
            semaphore.signal()
            group.leave()
        }
    }
    
    group.notify(queue: .main) {
        // what to do when everything is done
    }
}

An equivalent way to achieve the above is with a custom asynchronous Operation subclass (using the base AsynchronousOperation class defined here or here), e.g.

class BarOperation: AsynchronousOperation {
    private var item: Bar
    private var completion: ((Baz) -> Void)?

    init(item: Bar, completion: @escaping (Baz) -> Void) {
        self.item = item
        self.completion = completion
    }

    override func main() {
        asynchronousProcess(bar) { baz in
            self.completion?(baz)
            self.completion = nil
            self.finish()
        }
    }

    func asynchronousProcess(_ bar: Bar, completion: @escaping (Baz) -> Void) { ... }
}

Then you can do things like:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4

let completionOperation = BlockOperation {
    // do something with all the results you gathered
}

for item in array {
    let operation = BarOperation(item: item) { baz in
        // do something with result
    }
    operation.addDependency(completionOperation)
    queue.addOperation(operation)
}

OperationQueue.main.addOperation(completion)

And with both the non-zero semaphore approach and this operation queue approach, you can set the degree of concurrency to whatever you want (e.g. 1 = serial).

But there are other patterns, too. E.g. Combine offers ways to achieve this, too https://stackoverflow.com/a/66628970/1271826. Or with the new async/await introduced in iOS 15, macOS 12, you can take advantage of the new cooperative thread pools to constrain the degree of concurrency.

There are tons of different patterns.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for the response! However, the reason I have to have each operation run only after the previous is finished is because the api I'm using requires it (or it doesn't work) – swifter Aug 27 '21 at 06:28
  • 2
    This is one of the answers where know who wrote it after reading the first two sentences :) – Martin R Aug 27 '21 at 07:05
  • @swifter - If you need them to run serially, then you set the maximum concurrency to 1. But you can do this with semaphores (on background queue), operations, Combine, or async/await. Take your pick. – Rob Aug 27 '21 at 16:04
  • @Rob Thank you so much for your time and effort! This works amazingly. – swifter Aug 27 '21 at 19:40
0

you could try using swift async/await, something like in this example:

struct Xobj {
    func f(_ str: String) async {
        // something that takes time to complete
        Thread.sleep(forTimeInterval: Double.random(in: 1..<3))
    }
}

struct ContentView: View {
    var obj: Xobj = Xobj()
    let arr = ["one", "two", "three", "four", "five"]
    
    var body: some View {
        Text("testing")
            .task {
                await doSequence()
                print("--> all done")
            }
    }
    
    func doSequence() async  {
        for i in arr.indices { await obj.f(arr[i]); print("--> done \(i)") }
    }
}