Search code examples
iosobjective-cswiftgrand-central-dispatchfoundation

IOS - Difference between DispatchQueue.main.asyncAfter(deadline: .now()) and perform(_:with:afterDelay:) with 0 delay


I realized that there is a difference between using DispatchQueue.main.asyncAfter(deadline: .now()) and perform(_:with:afterDelay:0) when main queue is "busy".

Note that perform(_:with:afterDelay:) is called from main queue in my situation.

Seem like DispatchQueue.main.asyncAfter(deadline: .now()) performs the task immediately at the next run loop without caring about main queue but perform(_:with:afterDelay:) with 0 delay will wait and perform the task only when main queue is "free" (maybe it won't be called at the next run loop).

According to Apple document for perform(_:with:afterDelay:)

Specifying a delay of 0 does not necessarily cause the selector to be performed immediately. The selector is still queued on the thread’s run loop and performed as soon as possible.

I'm not sure that I understand them right so could anyone help me to explain exactly what is the difference under the hood between them? What does performed as soon as possible mean?

I found a same question here but seem like it's not what I want.


Solution

  • I created this standalone test to explore this topic.

    class ViewController: UIViewController {
    
        @objc func test1(_ info: Any) {
            guard let str = info as? String else { return }
            sleep(1)
            print(str)
    
            if str != "selector 3" {
                self.perform(#selector(test1), with: "selector 3", afterDelay: 0)
            }
    
            DispatchQueue.main.asyncAfter(deadline: .now()) {
                sleep(1)
                print("dispatch 4 queued by \(str)")
            }
    
        }
    
    
        @IBAction func test(_ sender: UIButton) {
            print("begin test")
    
            self.perform(#selector(test1), with: "selector 1", afterDelay: 0)
    
            DispatchQueue.main.asyncAfter(deadline: .now()) {
                DispatchQueue.main.asyncAfter(deadline: .now()) {
                    sleep(1)
                    print("dispatch 3")
                }
                sleep(1)
                print("dispatch 1")
            }
    
            self.perform(#selector(test1), with: "selector 2", afterDelay: 0)
    
            DispatchQueue.main.asyncAfter(deadline: .now()) {
                sleep(1)
                print("dispatch 2")
            }
    
            print("end test")
        }
    
    }
    

    Resulting output:

    begin test
    end test
    dispatch 1
    dispatch 2
    selector 1
    selector 2
    dispatch 3
    dispatch 4 queued by selector 1
    dispatch 4 queued by selector 2
    selector 3
    selector 3
    dispatch 4 queued by selector 3
    dispatch 4 queued by selector 3
    

    Things to observe:

    1. begin test and end test print before any other output showing that both perform(_:with:afterDelay:) and DispatchQueue.main.asyncAfter are queued and run later.
    2. The first two DispatchQueues run before the performs even though they are queued in a different order.
    3. All of the prints happen one second apart meaning they are all being run on the same queue waiting for the previous to finish.
    4. dispatch 3 doesn't jump ahead of selector 1 and selector 2 even though it is queued before dispatch 1 prints.

    Conclusions:

    1. Both Dispatch.main.asyncAfter and perform(_:with:afterDelay:) queue their selector/closure to be performed later. Since you are running perform(_:with:afterDelay:) on the main thread, it uses the main queue for its scheduling.
    2. For some unknown (to me) reason, Dispatch.main.asyncAfter(0) calls are queued before perform(_:with:afterDelay:0) calls when queued in the same run loop. Note: if any delay at all is added to the Dispatch.main.asyncAfter, it will then be queued after the perform(_:with:afterDelay:). Try using .now() + .milliseconds(1), for example.
    3. performed as soon as possible is just another way to say they are queued and processed in queue order. Depending on how long the tasks take in the queue ahead, it might take many runs of the run loop before a task is finally handled.