3

My DispatchQueue.main.asyncAfter execution block does not wait to execute.

I wrote a MacOS single view app. (xCode 12.0.1 (12A7300)). It has a for-loop that calls a function that downloads content from my server. I want to throttle the requests. I am trying to use DispatchQueue.main.asyncAfter. But all the calls in the for-loop are made instantly, at the same time. Here is my code:

func fetchDocuments() {
    for index in 651...660 {
        let docNumber = String(index)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
            print(Date())
            self.fetchDocument(byNumber: docNumber)
        }
    }
}

When I run this code I get this output on the console:

2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000
2020-10-05 03:27:09 +0000

I am running this code from Xcode and observing the console.

Any help will be appreciated.

sebrenner
  • 73
  • 1
  • 6
  • It does delay, but it delays all of them by 2 seconds. – New Dev Oct 05 '20 at 03:52
  • This question, using `asyncAfter` inside a `for` loop has been asked and answered many times. You might want to search yourself, but here’s a similar problem https://stackoverflow.com/a/62645769/1271826. – Rob Oct 05 '20 at 04:35
  • if you want to throttle the requests, then you can make a 'syncAfter' call on the completion of each download - but if what you really want is to download the files one at a time, in order, then you just make the call to download the next file in the completion handler of the current file – Russell Oct 05 '20 at 13:47

2 Answers2

1

The call to asyncAfter returns immediately, which means that as you speed through your loop, all of these iterations are effectively firing 2 seconds from now, rather than two seconds from each other.

There are secondary issues that when you use asyncAfter, it’s a little tedious to cancel them if the object is deallocated and you want to stop the process. Also, if you schedule all of these asyncAfter up front, you will be subject to timer coalescing (which will manifest itself when latter scheduled events are within 10% of each other; that's not a problem with a range of 651...660, but could manifest itself if you used larger ranges).

A couple of common solutions include:

  1. A recursive pattern will ensure that each iteration fires two seconds after the prior one finishes:

    func fetchDocuments<T: Sequence>(in sequence: T) where T.Element == Int {
        guard let value = sequence.first(where: { _ in true }) else { return }
    
        let docNumber = String(value)
        fetchDocument(byNumber: docNumber)
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
            self?.fetchDocuments(in: sequence.dropFirst())
        }
    }
    

    And call it like so:

    fetchDocuments(in: 651...660)
    
  2. Another approach is to use a timer:

    func fetchDocuments<T: Sequence>(in sequence: T) where T.Element == Int {
        var documentNumbers = sequence.map { String($0) }
    
        let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
            guard
                let self = self,
                let documentNumber = documentNumbers.first
            else {
                timer.invalidate()
                return
            }
    
            self.fetchDocument(byNumber: documentNumber)
            documentNumbers.removeLast()
        }
        timer.fire() // if you don't want to wait 2 seconds for the first one to fire, go ahead and fire it manually
    }
    

Both of these (a) will provide two second interval between each call, (b) eliminate timer coalescing risks; and (c) will cancel if you dismiss the object in question.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks, extremely useful. The recursive design is incredibly simple and effective (I had 300 steps of simulation so schedule). Question: what is precisely timer coalescing ? – claude31 May 23 '23 at 12:09
  • Timer coalescing is a power-saving feature by which if there are two or more events that are scheduled for some time in the future and their fire dates are within 10% of each other (and within a minute of each other, IIRC), the system will group them together, firing them off at the same time. It takes less power if the multiple timers are coalesced together. – Rob May 23 '23 at 15:27
0

'DispatchQueue.main.asyncAfter' is an asynchronous process. Here you have written'.now() + 2' for every statement. But your loop is executed very little time. So 2 seconds added with very little time for every statement inside the loop.

Try with this code below

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        fetchDocuments()
    }

    func fetchDocuments() {
        var count = 0
        for index in 651...660 {
            let docNumber = String(index)
            DispatchQueue.main.asyncAfter(deadline: .now() + (Double(count+1)*2.0)) {
                print(Timestamp().printTimestamp())
                self.fetchDocument(byNumber: docNumber)
            }
            count += 1
        }
    }
    
    func fetchDocument(byNumber: String) {
        print("Hello World")
    }
}


class Timestamp {
    lazy var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS "
        return formatter
    }()

    func printTimestamp() {
        print(dateFormatter.string(from: Date()))
    }
}

Output:

2020-10-05 10:41:18.473
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.475
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
2020-10-05 10:41:18.476
()
Hello World
AMIT
  • 906
  • 1
  • 8
  • 20
  • Rob, Thanks for pointing me in the right direction with the timer and the post on multiplying the delay by the index/count of the for-loop. I searched for SO but I wasn't using the right search terms. – sebrenner Oct 06 '20 at 01:30