pvllnspk
pvllnspk

Reputation: 5767

Swift Concurrency: `async let` strange behaviour on Simulators

I run the following code snippet and expect loadFromNetwork and loadFromDB to be run in parallel.

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task {
            async let networkData = loadFromNetwork()
            async let dbData = loadFromDB()
            await print("\(networkData) \(dbData)")
        }
    }
}

func loadFromNetwork() async -> String {
    print("loadFromNetwork started \(Thread.current)")
    // (0...10000000).forEach { String($0) }
    sleep(3)
    print("loadFromNetwork finished")
    return "Network Data"
}

func loadFromDB() async -> String {
    print("loadFromDB started \(Thread.current)")
    // (0...10000000).forEach { String($0) }
    sleep(1)
    print("loadFromDB finished")
    return "DB Data"
}

But I see that loadFromNetwork and loadFromDB run sequently on the same background thread. Why does it happen?

loadFromNetwork started <NSThread: 0x6000008d7180>{number = 3, name = (null)}
loadFromNetwork finished
loadFromDB started <NSThread: 0x6000008d7180>{number = 3, name = (null)}
loadFromDB finished
Network Data DB Data

UPDATE

I've found that this happens only on Simulators. I've tested on iPhone 13 Pro Max and iPhone 12 Prod Max. On my iPhone 7 device all works good. I've updated the title.

loadFromNetwork started <NSThread: 0x2834cc080>{number = 3, name = (null)}
loadFromDB started <NSThread: 0x2834c2280>{number = 5, name = (null)}
loadFromDB finished
loadFromNetwork finished
Network Data DB Data

Upvotes: 2

Views: 213

Answers (1)

matt
matt

Reputation: 535201

Your test is flawed. Don't mix threads and thread-sleep, on the one hand, with structured concurrency, on the other. Your use of sleep blocks. The whole point of structured concurrency await is that it doesn't block.

Here's a rewrite that tests properly:

extension Task where Success == Never, Failure == Never {
    static func sleep(_ seconds:Double) async {
        await self.sleep(UInt64(seconds * 1_000_000_000))
    }
    static func sleepThrowing(_ seconds:Double) async throws {
        try await self.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
    }
}


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

        Task {
            async let networkData = loadFromNetwork()
            async let dbData = loadFromDB()
            await print("\(networkData) \(dbData)")
        }
    }
}

func loadFromNetwork() async -> String {
    print("loadFromNetwork started \(Thread.current)")
    await Task.sleep(3.0)
    print("loadFromNetwork finished")
    return "Network Data"
}

func loadFromDB() async -> String {
    await Task.sleep(1.0)
    print("loadFromDB started \(Thread.current)")
    await Task.sleep(1.0)
    print("loadFromDB finished")
    return "DB Data"
}

My output is:

loadFromNetwork started <NSThread: 0x60000198bb40>{number = 5, name = (null)}
loadFromDB started <NSThread: 0x600001999500>{number = 7, name = (null)}
loadFromDB finished
loadFromNetwork finished
Network Data DB Data

As you can see, the subtasks are interleaved (concurrent), just as you desire.

Even so, looking at the thread number is still wrong. The system is perfectly free to use the same thread for all the different parts of these subtasks. For instance, if I delete the first sleep from loadFromDB, I get:

loadFromNetwork started <NSThread: 0x600002bf2480>{number = 7, name = (null)}
loadFromDB started <NSThread: 0x600002bf2480>{number = 7, name = (null)}
loadFromDB finished
loadFromNetwork finished
Network Data DB Data

So at least at the start they were using the same thread — and why not? We are still getting concurrency, which is all that matters. You shouldn't be peeking at what thread we are "on".

(I do call Thread.isMainThread sometimes just to make sure of how the context is being switched, but even that can risk generating a Heisenbug.)

Upvotes: 1

Related Questions