-
-
Save christianselig/3596716c876830b2f4683461be15d38a to your computer and use it in GitHub Desktop.
class ViewController: UIViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
Task { | |
// 1️⃣❓ UIViewController is in a MainActor context, so this Task | |
// will inherit that, so the following pretend expensive call will | |
// be on the main thread and likely block? | |
ExpensiveOperationPerformer.doExpensiveLoopAndPrint() | |
} | |
Task.detached { | |
// 2️⃣❓ Is this guaranteed to be off the main thread, so perhaps a | |
// better way to do a one-off, expensive operation? If it's not | |
// guaranteed, how would I ensure that? Wrap it in an actor | |
// instead of a class? What if it's not my class/I can't | |
// change the code? | |
ExpensiveOperationPerformer.doExpensiveLoopAndPrint() | |
} | |
Task { | |
ExpensiveOperationPerformer.printSomeNetworkData() | |
} | |
} | |
} | |
class ExpensiveOperationPerformer { | |
@MainActor | |
static func printSomeNetworkData() async throws { | |
let url = URL(string: "https://example.com/example.json")! | |
// 3️⃣❓ Will this time consuming network call be guaranteed to NOT | |
// execute on main, despite the MainActor context? Or will it block? | |
let (data, response) = try await URLSession.data(for: url) | |
print(data) | |
} | |
static func doExpensiveLoopAndPrint() async { | |
let upperEnd = 9_999_999_999 | |
var sum = 0 | |
for i in 0 ..< upperEnd { | |
sum += 1 | |
} | |
print(sum) | |
} | |
} |
Thank you so much for taking the time to explain all this! I've learned a lot! ❤️
❤️
Correction: I tried to prove my claims with some sample code and it turns out NSURLConnection
does use a separate thread for something. See the screenshot: when I set a breakpoint in one of the delegate calls, there is a com.apple.NSURLConnectionLoader
thread:
It's possible this thread does things like setting up the connection, HTTP parsing, etc. This doesn't invalidate everything I said above because it's definitely possible to write asynchronous networking code without any sort of multithreading (e.g. by using the socket APIs directly), but apparently that's not what NSURLConnection
does. Sorry for leading you on the wrong track.
You can also see in the screenshot in the main thread's stack trace that it's using the run loop to invoke the delegate. __CFRunLoopDoSource
sounds like it's invoking a callback for a run loop source that has new events (the socket).
Here's my code (macOS Command Line Project in Xcode):
import Foundation
final class Loader: NSObject, NSURLConnectionDataDelegate {
var connection: NSURLConnection? = nil
func start() {
let url = URL(string: "https://google.com")!
connection = NSURLConnection(request: URLRequest(url: url), delegate: self)
}
func connection(_ connection: NSURLConnection, didReceive data: Data) {
print(#function, data)
}
func connection(_ connection: NSURLConnection, didFailWithError error: Error) {
print(#function, error)
}
}
let loader = Loader()
loader.start()
RunLoop.current.run()
I'm quite sure no other thread in your app is involved. The actual download is being performed by the networking subsystem of the OS (however that works) and is being managed by the kernel (I think). When new data becomes available, the kernel informs your app that new data on the socket is available. No multithreading required.
NSURLConnection(request:delegate:)
isn't modern networking, but I'd imagine the general principle would be the same forURLSession
. The difference might be (I don't know) thatURLSession
could be using GCD dispatch sources or similar (instead of the run loop) to handle notifications from the socket and then calls your callbacks on a background queue (instead of the thread that started the connection).