Duncan C
Duncan C

Reputation: 131501

My async code is executing in the wrong order. Why is that?

Here is my async code:

func doAsyncStuff() {
    //Run a loop on the main thread
    for callCount in 1...5 {

        //#1 inside loop, about to call asyncFunc
        print("About to call asyncFunc. callCount = \(callCount)")

        asyncFunc(callCount: callCount) { result in
            //MARK: - This is the completion handler
            //#2 In completion handler after async function finishes
            print("In completion handler. Result = \(result). callCount = \(callCount)")
            //MARK: - End of completion handler
        }
    }
}

func asyncFunc(callCount: Int, completion: @escaping (Bool) -> ()) {
    //#3 Entering asyncFunc
    print("  Entering \(#function). callCount = \(callCount)")

    //#4 Pick a random delay value from 0.01 to 0.2
    let delay = Double.random(in: 0.01...0.2)
    print(String(format: "    About to call asyncAfter with delay %.2f", delay))

    //#5 asyncAfter() call
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        //MARK: - This is the closure code that is run after a delay
        completion(Bool.random())
        //MARK: - End of closure code
    }

    //#6 Leaving asyncFunc
    print("  Leaving \(#function). callCount = \(callCount)")
}

The output should look like this:

About to call asyncFunc. callCount = 1
  Entering asyncFunc(callCount:completion:). callCount = 1
    About to call asyncAfter with delay 0.16
    In completion handler. Result = true. callCount = 1
  Leaving asyncFunc(callCount:completion:). callCount = 1
About to call asyncFunc. callCount = 2
  Entering asyncFunc(callCount:completion:). callCount = 2
    About to call asyncAfter with delay 0.05
    In completion handler. Result = true. callCount = 2
  Leaving asyncFunc(callCount:completion:). callCount = 2

That is not what happens at all. Why is that?

Upvotes: 1

Views: 74

Answers (1)

Duncan C
Duncan C

Reputation: 131501

Asynchronous code is, well, asynchronous! An async method runs separately from the main loop. The function call

    DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
        //MARK: - This is the closure code that is run after a delay
        completion(Bool.random())
        //MARK: - End of closure code
    }

Returns immediately. The code inside the braces (the completion handler) gets saved away and run at a later time.

This code is written to simulate submitting several network calls submitted one right after the other, without waiting for any of them to complete.

Instead of network calls, this example program uses calls to DispatchQueue.main.asyncAfter(deadline:execute:) with a random small delay before it runs the completion handler.

When you submit several async requests in a row (like network requests, for example), the unpredictable network response time means you can't know in what order those calls will complete. They very likely will not complete in the order you submit them, and none of them will complete before the next statement after the async call is run. Sample output from this program might look like this:

About to call asyncFunc. callCount = 1
  Entering asyncFunc(callCount:completion:). callCount = 1
    About to call asyncAfter with delay 0.16
  Leaving asyncFunc(callCount:completion:). callCount = 1
About to call asyncFunc. callCount = 2
  Entering asyncFunc(callCount:completion:). callCount = 2
    About to call asyncAfter with delay 0.05
  Leaving asyncFunc(callCount:completion:). callCount = 2
About to call asyncFunc. callCount = 3
  Entering asyncFunc(callCount:completion:). callCount = 3
    About to call asyncAfter with delay 0.14
  Leaving asyncFunc(callCount:completion:). callCount = 3
About to call asyncFunc. callCount = 4
  Entering asyncFunc(callCount:completion:). callCount = 4
    About to call asyncAfter with delay 0.07
  Leaving asyncFunc(callCount:completion:). callCount = 4
About to call asyncFunc. callCount = 5
  Entering asyncFunc(callCount:completion:). callCount = 5
    About to call asyncAfter with delay 0.09
  Leaving asyncFunc(callCount:completion:). callCount = 5
In completion handler. Result = true. callCount = 2
In completion handler. Result = true. callCount = 4
In completion handler. Result = true. callCount = 5
In completion handler. Result = true. callCount = 3
In completion handler. Result = true. callCount = 1

This sample code logs the delay amounts it uses for each call. The call with the shortest delay will run first. The call with the next shortest delay will run next, etc.

You can download the sample project from Github and try it out for yourself. Set breakpoints at various points and step through the code.

Upvotes: 2

Related Questions