Implementing a completion handler for an asynchronous send queue that calls asynchronous methods

I have a performSync function that goes through an array and for each element in this array, I call a function that itself contains an async alamofire request. I need to know when this external function completed execution of all the functions of the for loop, so I need to add a completion handler. I am not sure how to implement this.

Also, in my for loop, I used .userinitiated to try not to block the ui stream, however the stream is blocked. I also tried using the parallel queue method with comments, but also blocked the user interface.

Code below:

public class Sync { public class func onPerformSync(finished: () -> Void){ let syncList = ['Process1', 'Process2', 'Process3'] //let concurrentQueue = DispatchQueue(label: "concurrentQueue", attributes: .concurrent) for item in syncList { //concurrentqueue.async DispatchQueue.global(qos: .background).async { let className = "appname."+item let ClassObj = NSClassFromString(className)! as! Helper_Base.Type ClassObj.doSync() } //multiple classes extend Helper_Base and override the doSync func //I realise there may be a swiftier way to call these doSync methods rather than instantiating from a string and overriding the base class //doSync and I'd welcome advice on it! } //calling finished here is fine if I use a synchronous dispatchQueue but where do i place this line since I need my queue to be asynchronous? finished() } } open class Process1 : Helper_Base { override open class func doSync(){ let time = Int64(NSDate().timeIntervalSince1970 * 1000) repeatBlock(time: time) } open class func repeatBlock(time : Int64){ let parameters : [String : String] = [ "x" : time ] var continueSync : Bool = false DispatchQueue.global(qos: .background).async { Alamofire.request(url, method: .post, parameters: parameters, encoding: URLEncoding.default) .response { response in if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) { guard let utf8TextDecoded = utf8Text.fromBase64() else { return } let error = response.error if error == nil { do { let json = try JSONSerializer.toDictionary(utf8TextDecoded) let more = json["more"] as! Bool continueSync = more } } } if continueSync { let time = Int64(NSDate().timeIntervalSince1970 * 1000) DispatchQueue.global(qos: .background).async { repeatBlock(time: time) } } else{ finishSync() } } } } open class func finishSync(){ //other stuff } } Sync.onPerformSync(){ print("we're done syncing") } 
+1
source share
2 answers

A few observations:

  • In response to your basic question of how to finishSync tell your caller that this was done, you are likely to refactor doSync to take a completion handler. This (because it makes recursive network calls) assumes that you can go to instance methods and save completionHandler :

     public class HelperClass { private var completionHandler: (() -> Void)? open func doSync(item: String, completionHandler: @escaping () -> Void) { guard self.completionHandler == nil else { // make sure the existing completionHandler is nil fatalError("Either use another instance or let this one finish first") } self.completionHandler = completionHandler // save the completion handler repeatBlock() // start your recursive code } open func repeatBlock() { // do your recursive stuff // do something } open func finishSync(){ // other stuff completionHandler?() // call the saved completion handler completionHandler = nil // and discard it, removing any strong references, if any } } 
  • This only then asks the question of how to ensure that the top-level for process1 for process1 , process2 , etc. does not start these cycles at the same time. Honestly, you would usually want it to run at the same time (because you pay a huge penalty for executing queries sequentially), but if you do not, you will either have to wrap it in another recursive request process, or wrap it all this in an asynchronous Operation user subclass.

  • Regarding why your user interface is blocked, this is less clear. In any case, you do not even need to send doSync to any global queue, because Alamofire is asynchronous. It makes no sense to send a call that is already asynchronous to the background queue.

    The only thing that looks suspicious is a recursive call from repeatBlock back to repeatBlock . If an Alamofire call does not start asynchronously (i.e., it returns caching results or some error), theoretically you could spin in the queue that Alamofire uses. I would suggest sending a recursive call to repeatBlock from repeatBlock using async to avoid possible problems.

    Another thing you can do is provide the queue parameter for the Alamofire request , ensuring that it will not use the main queue. Going to his own devices, he calls completion handlers in the main queue (which is usually very useful, but can cause problems in your script). I suggest trying to supply a non-primary queue as the queue request parameter.

    If it still blocks, I suggest either running it on the system track of the tools and see which blocking calls are in the main queue. Or you can just start the application, pause it while the user interface is locked, and then look at the stack trace for the main thread, and you can see where it blocks.

    Finally, we should consider the possibility that the main queue lock will not be saved in the above code. Assuming that you are not suffering from a degenerate situation where Alamofire immediately calls the completion handler, the above code is unlikely to block the main queue. The above diagnostics should confirm this, but I could suggest you expand your search to identify other things that would block the main queue:

    • Any sync calls from a serial queue (for example, the main queue) to a serial queue are a likely problem.
    • Candidates for any locks, semaphores, or send groups are also likely.
    • You have ways in which you don't call repeatBlock recursively and don't call finishSync ... you are sure that the main queue is locked, and this is not just a question that it never calls finishSync in some execution paths. "

    Bottom line, make sure that the problem actually lies in blocking the main thread in the above code. I suspect this is not the case.

In order to close a small portion of unsolicited advice and not cause any damage, we must admit that there is a hint of code smell in this stream of network requests. If you want to return more data, change the web services API to return more data ... this repeated systematic selection of additional data (especially when executed sequentially) is terribly inefficient. Network latency will do overall performance (even after solving this problem with blocking the main queue).

+1
source

I had similar problems - and I used a global variable (singleton) to increase the counter for each new request that was launched, and then decrease that counter in each completion handler.

If you get to myQueueCounter == 0 , then you are done and you can call the doneSyncing method

if there is a risk that some of the queues may be completed before they are all started, then you may need an additional variable for everythingHasStarted = true

0
source

All Articles