Async/await thread

considering this code:

let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)

func foo() {
    async {
        let url = URL(string: "https://www.apple.com")!
        let (data, response) = await try! session.data(from: url, delegate: nil)
    }
}

what thread/queue is the async's closure parameter run on and is it possible to opt-in to a specific queue/thread? smth like:

dispatchPrecondition(condition: .onQueue(.main))
async(queue: .main) { // pseudo code
        dispatchPrecondition(condition: .onQueue(.main))
        let (data, response) = await try! session.data(from: url, delegate: nil)
        dispatchPrecondition(condition: .onQueue(.main))
}

Unless there is an enclosing actor such as @MainActor you shouldnā€˜t make any assumption on which thread it may run. As far as I understood the system is free to resume the suspended call even on the same thread it was before, but its not a guarantee. However by @David_Smith the >default< dispatch queue for the general non-isolated async execution will be a global concurrent queue. In the future we should gain more control over this through custom executors.

ouch. i assumed async/await and threads are more orthogonal and less inter-independent. does that mean it is not possible to use swift's async/await in a single-threaded application? (FTM: this is definitely possible with the promise-based async/await implementation.)

That question I cannot answer myself. Letā€™s hope someone else from the community would join and provide a detailed explanation to that.

does that mean it is not possible to use swift's async/await in a
single-threaded application?

Can you be more specific about what you mean by ā€œsingle-threaded applicationā€? Very few applications on Apple platforms are truly single threaded because Appleā€™s frameworks regularly spin up threads (either explicitly or implicitly using Dispatch).

Based on the code snippet you posted it sounds like you want to run your URLSession code async on the main actor and thatā€™s definitely possible. However, I suspect that your main queue stuff was just a straw man and thereā€™s more going on here.

Share and Enjoy

Quinn ā€œThe Eskimo!ā€ @ DTS @ Apple

it is understandable that system can span threads/queues under the hood. i used the term "single-threaded application" somewhat loosely. consider this example:

import UIKit

class ViewController: UIViewController {
    var session: URLSession!
    let url = URL(string: "https://www.apple.com")!
    let queue = DispatchQueue(label: "serialQueue") // 5a
    // let queue = DispatchQueue.main // 5b

    override func viewDidLoad() {
        super.viewDidLoad()
        // 0
        queue.async {
            // 1
            self.foo()
        }
    }
    
    func foo() {
        // 2
        dispatchPrecondition(condition: .onQueue(queue))
        let opQueue = OperationQueue()
        opQueue.underlyingQueue = queue
        session = URLSession(configuration: .default, delegate: self, delegateQueue: opQueue)
        session.dataTask(with: url) { data, response, error in
            // 3
            dispatchPrecondition(condition: .onQueue(self.queue))
            // do something else here
        }.resume()
    }
}

extension ViewController: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        // 4
        dispatchPrecondition(condition: .onQueue(queue))
    }
}

from the app perspective (1) -- (4) are executed on a single serial queue, so there is no need to protect a shared mutable state with mutexes or do some other synchronisation to read/modify it. furthermore if you change (5a) to (5b) - then all (0) -- (4) are executed on the main thread, as well as any other line of code of this application (i'm not talking about what system is doing internally). so speaking very loosely this is either a "two queue application" (with 5a) or a "single threaded application" (with 5b).

assuming these somewhat loose definitions is it possible to have swift async/await based application every line of code of which runs on main thread? or more generally, on the specific threads/queues that the app specifies? with promise based async-await implementation it is definitely possible (as it's just a long winded syntax sugar above callbacks and doesn't introduce it's own opinion about threads/queues).

The snippet you posted (with 5a enabled) could run into The Deallocation Problemā„¢ because youā€™re letting references to a non-thread object (ViewController) escape into a threaded context (all the blocks you run on self.queue). IMO the best way to avoid this is to separate any threaded code out of your view controllers into a class that you know is safe to deallocate off the main thread. And that technique lines up well with Swift concurrencyā€™s actor model. So, in your example Iā€™d pull everything that runs on queue out into an actor, which gets you the serialisation you want while helping to avoid The Deallocation Problemā„¢.

With regards your 5b case, youā€™d generally approach this by defining all your code to run on the main actor.

Share and Enjoy

Quinn ā€œThe Eskimo!ā€ @ DTS @ Apple

good catch. a new version:

import UIKit

@MainActor
class Controller: NSObject {
    var session: URLSession!
    let url = URL(string: "https://www.apple.com")!
    let queue = DispatchQueue.main

    override init() {
        super.init()
        
        dispatchPrecondition(condition: .onQueue(queue))
        async {
            // WARNING! some system queue here?!
            // dispatchPrecondition(condition: .onQueue(queue)) fails
            let data = await foo()
            dispatchPrecondition(condition: .onQueue(queue))
            // do something with data
        }
    }
    
    private func foo() async -> Data {
        dispatchPrecondition(condition: .onQueue(queue))
        let opQueue = OperationQueue()
        opQueue.underlyingQueue = queue
        session = URLSession(configuration: .default, delegate: self, delegateQueue: opQueue)
        let data = try! await session.data(from: url, delegate: self)
        dispatchPrecondition(condition: .onQueue(queue))
        return data.0
    }
}

extension Controller: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        dispatchPrecondition(condition: .onQueue(queue))
        return (.performDefaultHandling, nil)
    }
}

class ViewController: UIViewController {
    var controller: Controller!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        controller = Controller()
    }
}

i presume that for this question:

is it possible to have swift async/await based application every line of code of which runs on main thread?

the answer is "no"? given that async closure runs on some system queue that i have no ability to specify. see the "WARNING!" lines in code.

i presume that for this question ā€¦ the answer is "no"?

Thatā€™s not correct. The trick here is the @MainActor attribute, as discussed here. Consider this snippet:

@IBAction
private func testAction(_ sender: Any) {
    NSLog("testAction(_:)")
    async { @MainActor in
        NSLog("closure")
        await self.delay()
    }
}

private func delay() async {
    NSLog("delay()")
    await Task.sleep(1 * 1000 * 1000 * 1000)
}

This prints:

2021-06-30 09:50:38.913443+0100 xxsi13[54758:3760684] testAction(_:)
2021-06-30 09:50:38.914759+0100 xxsi13[54758:3760684] closure
2021-06-30 09:50:38.914915+0100 xxsi13[54758:3760684] delay()

Note how the thread ID (3760684) is the same in all cases.

IMPORTANT I tested this in Xcode 13.0b2 targeting the iOS 15.0 simulator. To get it to work I had to set the SWIFT_DEBUG_CONCURRENCY_ENABLE_COOPERATIVE_QUEUES environment variable, but I believe thatā€™s because of an implementation bug rather than anything fundamentally wrong with my code.

Having said that, this stuff is moving very fast so I could be wrong (-:


Oh, one last thing: The Swift concurrency design has an affordance for something called a custom executor, which will give you more control over how actors integrate with queues (or threads). However, this isnā€™t yet designed, let alone implemented. If youā€™re curious, see Support custom executors in Swift concurrency.

Share and Enjoy

Quinn ā€œThe Eskimo!ā€ @ DTS @ Apple

5 Likes

thank you,

"async { @MainActor in"

indeed made the trick. it's a bit of a footgun that async brings you to some random queue by default but that's not the end of the world.

good to know. looking forward to seeing it.