Search code examples
iosswiftmultithreadinggrand-central-dispatchxctest

How does wait succeed for a block that is to be executed on the next dispatch?


import XCTest
@testable import TestWait

class TestWait: XCTestCase {
    
    func testX() {
        guard Thread.isMainThread else {
            fatalError()
        }
        let exp = expectation(description: "x")
        DispatchQueue.main.async {
            print("block execution")
            exp.fulfill()
        }
        print("before wait")
        wait(for: [exp], timeout: 2)
        print("after wait")
    }
}

Output:

before wait
block execution
after wait

I'm trying to rationalize the sequence of the prints. This is what I think:

  1. the test is ran on main thread
  2. it dispatches a block off the main thread, but since the dispatch happens from the main thread, then the block execution has to wait till the current block is executed
  3. "before wait" is printed
  4. we wait for the expectation to get fulfilled. This wait sleeps the current thread, ie the main thread for 2 seconds.

So how in the world does wait succeed even though we still haven't dispatched off of main thread. I mean "after wait" isn't printed yet! So we must still be on main thread. Hence the "block execution" never has a chance to happen.

What is wrong with my explanation? I'm guessing I it must be something with how wait is implemented


Solution

  • The wait(for:timeout:) of XCTestCase is not like the GCD group/semaphore wait functions with which you are likely acquainted.

    When you call wait(for:timeout:), much like the GCD wait calls, it will not return until the timeout expires or the expectations are resolved. But, in the case of XCTestCase and unlike the GCD variations, inside wait(for:timeout:), it is looping, repeatedly calling run(mode:before:) until the expectations are resolved or it times out. That means that although testX will not proceed until the wait is satisfied, the calls to run(mode:before:) will allow the run loop to continue to process events (including anything dispatched to that queue, including the completion handler closure). Hence no deadlock.

    Probably needless to say, this is a feature of XCTestCase but is not a pattern to employ in your own code.

    Regardless, for more information about how Run Loops work, see the Threading Programming Guide: Run Loops.