Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to perform multiple asynchronous requests starting one after another

I have a set of requests that are performed asynchronous. However, each next request should only start when the previous request is finished (due to data dependency).

Since all requests should just be finished in the right order, DispatchGroup() seems of no use.

I currently implemented DispatchSemaphore(), but I feel that this is not the best solution since I want to assure that all requests are performed in the background.

let semaphore = DispatchSemaphore(value: requests.count)

for request in requests {
    apiManager().performAsyncRequest(request, failure: { error in
        print(error); semaphore.signal()
        }) { print(“request finished successful”) 
        // Next request should be performed now
        semaphore.signal()
    }
}
semaphore.wait()

Is there a better way to perform this?

Note: based on implementation of one of the answers below I encountered that apiManager() is not thread safe (due to the use of a Realm database).

To keep this question clear and consider the answers in a thread safety manner with a thread safe definition of performAsyncRequest:

public func performAsyncRequest(_ requestNumber: Int, success: @escaping (Int) -> Void)->Void {
    DispatchQueue(label: "performRequest").async {
        usleep(useconds_t(1000-requestNumber*200))
        print("Request #\(requestNumber) starts")
        success(requestNumber)
    }
}

Solution with DispatchSemaphore

        let semaphore = DispatchSemaphore(value: 1)
        DispatchQueue(label: "requests").async {
            for requestNumber in 0..<4 {
                semaphore.wait()
                performAsyncRequest(requestNumber) { requestNumber in
                        print("Request #\(requestNumber) finished")
                        semaphore.signal()
                }
            }
        }

With the expected output:

Request #0 starts
Request #0 finished
Request #1 starts
Request #1 finished
Request #2 starts
Request #2 finished
Request #3 starts
Request #3 finished

Unsuccessful try with Operation

    var operations = [Operation]()

    for requestNumber in 0..<4 {
        let operation = BlockOperation(block: {
            performAsyncRequest(requestNumber) { requestNumber in
                DispatchQueue.main.sync {
                    print("Request #\(requestNumber) finished")
                }
            }
        })

        if operations.count > 0 {
            operation.addDependency(operations.last!)
        }
        operations.append(operation)
    }

    let operationQueue = OperationQueue.main
    operationQueue.addOperations(operations, waitUntilFinished: false)

With incorrect output:

Request #0 starts
Request #1 starts
Request #2 starts
Request #3 starts
Request #0 finished
Request #3 finished
Request #2 finished
Request #1 finished

My feeling is that it should also be possible to get this working with Operation, but I don't know whether it would be better than using DispatchSemaphore.

like image 560
Taco Avatar asked Oct 30 '22 19:10

Taco


2 Answers

You are on the right track with the DispatchSemaphore to ensure that an asyncronous call is not started before the previous one has finished. I would just ensure that the code that manages the calls to the asyncronous API runs in the background:

let backgroundQueue = DispatchQueue(label: "requests")
let semaphore = DispatchSemaphore(value: 1)

backgroundQueue.async {
    var requestNumber = 1

    for request in requests {
        semaphore.wait()

        let currentRequestNumber = requestNumber

        print("Request launched #\(requestNumber)")

        apiManager().performAsyncRequest(request,
        failure: {
            error in
            print("Request error #\(currentRequestNumber)")
            semaphore.signal()
        }) {
            print("Request result #\(currentRequestNumber)")
            semaphore.signal()
        }

        requestNumber = requestNumber + 1
    }
}

The code will continue execution immediatelly while the for loop runs in a background loop and starts each request after waiting for the previous one to finish.

Or if apiManager() is not thread safe:

let semaphore = DispatchSemaphore(value: 1)

var requestNumber = 1

for request in requests {
    semaphore.wait()

    let currentRequestNumber = requestNumber

    print("Request launched #\(requestNumber)")

    apiManager().performAsyncRequest(request,
    failure: {
        error in
        print("Request error #\(currentRequestNumber)")
        semaphore.signal()
    }) {
        print("Request result #\(currentRequestNumber)")
        semaphore.signal()
    }

    requestNumber = requestNumber + 1
}

This has the limitation that the for loop will be in execution until the last request starts execution. But if the code you are calling is not thread safe there is no way around that.

like image 99
xpereta Avatar answered Nov 15 '22 07:11

xpereta


You can use NSOperationQueue and add each request as as operation

let firstOperation: NSOperation
let secondOperation: NSOperation
secondOperation.addDependency(firstOperation)

let operationQueue = NSOperationQueue.mainQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
like image 32
Misha Avatar answered Nov 15 '22 07:11

Misha