Do async operations always run on a background thread?

Would like to believe.

TBH Swift Concurrency is incomprehensible to me. I have tried to read/watch multiple explanatory texts/videos, including personal(!) explanatory posts from Apple developers on this forum, and from @mattie the expert in Swift Concurrency, and still do not understand when I should use Swift Concurrency (and if should I at all).

await in my opinion is worthless. I see it like a simple synchronous call. The execution is paused until function returns. OK, the calling thread is "freed" while awaiting and can do other "concurrent" things, but these things are not actually concurrent. They are divided in quite large "chunks" divided between awaits. What if I do not include awaits in the called async function? This is especially true when async functions inherit the caller isolation (Main Actor, I looking at you). Should we do everything in one thread?

I see much more sense in parallel execution provided by GCD.

Yeah, I hear you.

That having been said, for me, Swift concurrency has been a godsend. For example, I had an old, legacy app with a lot of network code with delegates/protocols, completion handlers, etc.: I’ve been able to refactor it with Swift concurrency and it now is much simpler, far less code, and far easier to reason about. It’s been great.

Where I criticize the evolution of Swift concurrency is the (premature?) adoption of sendability, the attempt to mitigate these hassles with region-based isolation, the fact that they encouraged us to use this new paradigm before so many of their own frameworks properly supported it, etc. Frequently, the cure feels worse than the disease. To mix metaphors, it feels like an every year is an exercise of “one step back and two steps forward.”

I’ve never spent so much time and effort in my career working around the limitations of a platform’s own frameworks. I can only imagine the total loss of productivity across our industry figuring out work-arounds for framework limitations. I think this new “Approachable Concurrency” is a step in the right direction, but it has been a painful few years. I haven’t always been happy playing on the bleeding edge of the platform. But Swift concurrency is brilliant and I appreciate everything that the Swift team has been trying to achieve. I can clearly see where they are going: We’re not quite there, but it is tantalizingly close.

That having been said, GCD still has a place in our toolchest. It excels at performing massively parallelized, synchronous, compute-intensive calculations. Apple has even explicitly advised to keep this sort of code in GCD, and bridge back to Swift concurrency with continuations.

But to your broader point, if you’re happier using GCD in your personal projects, then just do that. But I might encourage you to not give up on Swift concurrency quite yet, because it has so many advantages and is undeniably the future of the language.

5 Likes

I know you're being sarcastic, but very often the answer is yes. We got surprisingly large systemwide performance wins in iOS in years past by reducing the amount of concurrency[1]. Single-threaded async is a completely legitimate approach in many cases.

As an early skeptic of the new approach, one of the things that won me over was realizing (thanks to @hborla's explanations) that the previous model encouraged people to do thread hops without considering whether that's something they wanted, something that I discouraged with libdispatch[2].

My hope is that the new design will lead to concurrency being used more intentionally[3], rather than reflexively or even accidentally.


  1. which was libdispatch-based at the time ↩︎

  2. e.g. any of the threads here where I've argued against the dispatch_barrier_async + concurrent queue pattern for guarding mutable state ↩︎

  3. dare I hope… after measuring first? ↩︎

5 Likes

Sounds great. But I really don't understand Swift Concurrency. If for example a network request takes 1 second, why my UI won't be frozen for 1 second? Given that the network request is dispatched on main thread between two awaits, and there are no more awaits in my code, especially inside the network request? Or there are lots of awaits in the network request code (which is not my code)? Even if so, how do I know that?

IO is a really interesting topic, since it’s not running on the CPU at all but rather waiting for something else, which comes with a number of fascinating constraints and opportunities.

But in this particular example: I would be very surprised if an async network API was designed to do a blocking wait for the network on the main actor, and would definitely file a bug about it if I did discover that. There’s just no reason I can think of that anyone would make that choice.

Waiting in main thread in this example. IMHO UI will be frozen.

If you haven’t seen it already, I might suggest starting with watching Meet async/await in Swift or any of the videos on that page. Also, if you’re proficient with GCD, I think the Swift concurrency: Update a sample app video is a good primer on how to refactor GCD code to adopt Swift concurrency. There are a ton of great WWDC videos over the years. And each video has links to other great videos.

No, it will not be frozen. If you’re looking for something about how network requests work with Swift concurrency, perhaps the Use async/await with URLSession video might be a good starting point.

But this is the beauty of Swift concurrency. When your code reaches the await of the network code, your function is suspended while the network request is underway, but the main thread is still free to keep a responsive UI while the network request is underway.

I must confess that, at this point, you’ve kind of lost me. It’s hard to follow you without code snippets.

So, I might suggest (a) watching a few of those videos, if you haven’t already; and then (b) if you still have questions, maybe post your own question, with representative code snippets, so we can better follow what is going on. I wager that once you watch a few of those videos, you might not even need us at that point (or if you do, they’ll be minor implementation details, not fundamental “how does Swift concurrency work” questions).

And, please don’t take this the wrong way, but this is starting to feel a bit tangential to danmihai’s original question. Further follow-up on your questions probably don’t belong here, under this thread, but perhaps should be posted as a new topic (and with code snippets, so we can better follow what you are talking about).

1 Like

Back to original question: "Do async operations always run on a background thread?" My understanding is no, they well may be dispatched to main thread, especially after introducing SE-0420. And this bothers me because I would prefer to keep main thread primarily for UI only.

It’s not at random though. It’s not “sometimes it will just put things on the main thread for no reason”. If you want things on a background thread there’s nothing stopping you.

All this information is the part of function signature now. So even with third-party library with closed code you can reason about where the code will be executed. This is much clearer than GCD black box in that way.

With GCD you can also dispatch to the main thread, even in a blocking manner. And vice-versa. So another advantage of Swift Concurrency is that it’s hard to block just using it: there is no sync method akin to DispatchQueue.

The answer is a definitive “no”. (And I apologize if I was unclear, as I tried to say that several times. Perhaps it got lost in my long answers.)

Yes, SE-0420 is an example. As is SE-0461. But we don’t need to get lost in these complicated proposals: You can simply await a call to a function that happens to be isolated to the main actor. Technically, the fact that it is an asynchronous operation has nothing to do with it being on a background thread or not. Now, clearly, if you get down to something that is ultimately slow and could block the thread, even fleetingly, that sort of stuff is generally off on a background thread. But it’s not as simple as “if I await, it must be on a background thread.”

Like you, many of us have that deeply-ingrained intuition, but Swift 6.2, in particular, may challenge your expectations in this regard. I’d suggest that you soak in their vision doc, Improving the approachability of data-race safety.

1 Like

My intuition tells me that there may be a long running operation (yes, isolated to main actor), and there may not be enough awaits (suspension points) which yield execution and divide this long running operation into concurrent chunks (the whole operation may occur in one chunk between two awaits). In this case UI will be frozen long enough for user to notice it.

All my recent posts in several forum threads were about that, sorry for the noise.

I already said that I don't understand Swift Concurrency well enough, and maybe I am all wrong. I'm just trying to understand how to use the thing. Many of replies I got were akin to "it just works", which do not improve my understanding.

Probably I need to ensure that long running operations are not isolated to main actor, but this is about how to avoid the problem. My question though was not about avoiding the problem, but rather "Does the problem exist?" - regarding main actor, when some concurrent code is isolated to it and we have a long running operation.

Intuitively I feel that a long running synchronous operation should cause a delay somewhere (good if not on the UI thread). Knowledgeable people on this forum keep telling me that concurrently dispatching anything to main actor will not cause freezing the UI. Nobody explains how this magic works. Does this magic even exist?

There is no magic here :smile: A long-running synchronous operation can, and will, block other work — and if it happens on the main thread, then it will cause the main thread to be delayed in processing other tasks (like your UI).

The key part is that, in the case of an operation like this:

The network request, unless you make it yourself using synchronous networking APIs, is not being made on the main thread — specifically to avoid blocking it. URLSession, for instances, manages its work on a dedicated background thread(s) so that you can make requests and have them not block other work. While you await the request, the thread is then available to take on more work.

If instead, you used a different API like the long-deprecated NSURLConnection.sendSynchronousRequest(_:returning:), then you would expect to see a delay in execution (and possibly blocking your UI). From the docs:

Important
Because this call can potentially take several minutes to complete (particularly when using a cellular network in iOS), you should never call this function from the main thread of your application. Doing so may cause a 0x8badf00d exception with the main thread at mach_msg_trap. The solution is to migrate to URL loading using URLSession.

Note that this was written long before Swift Concurrency, and it remains true: Swift Concurrency is a cooperative system, and it's up to individual parties to manage their work correctly to avoid these sorts of blocks.

This is a great question. The main way to find out is to read the documentation of the tools you're using. The URLSession docs, for example, have a section on asynchronicity:

Asynchronicity and URL sessions
Like most networking APIs, the URLSession API is highly asynchronous. It returns data to your app in one of three ways, depending on the methods you call:

  • If you’re using Swift, you can use the methods marked with the async keyword to perform common tasks. For example, data(from:delegate:) fetches data, while download(from:delegate:) downloads files. Your call point uses the await keyword to suspend running until the transfer completes. You can also use the bytes(from:delegate:) method to receive data as an AsyncSequence. With this approach, you use the for-await-in syntax to iterate over the data as your app receives it. The URL type also offers covenience methods to fetch bytes or lines from the shared URL session.

In general, there's no foolproof way to know — but for code written in Swift, using Swift Concurrency, the compiler does a lot of work to make it very difficult to get things wrong. When interfacing with non-Swift APIs, or with low-level synchronization primitives that don't cooperate with Swift Concurrency, you need to be a bit more careful.

But, because of this, and because the vast majority of APIs you're likely to use on a regular basis are written with concurrency in mind, it can be difficult to trigger the types of blocking you're worried about.

1 Like

If you have a long-running operation that is not doing anything synchronous, but is merely awaiting other stuff, then, no, it doesn’t matter if that happens to be on the main actor or not. It is not a question of how many awaits there are. It doesn’t matter how long it must await for a result. It doesn’t matter how many awaits there are. It’s only a question of whether the routine does anything slow and synchronous.

In short, if doing anything synchronously, then get that off the main actor. If not doing anything synchronously (just awaiting other stuff, for example), then it doesn’t matter.

1 Like

Any code between awaits is synchronous.

Yes, but if doesn’t block the thread, if it is not slow, then it is generally inconsequential.

Hey, do whatever you want, but one of the ideas of the “Approachable concurrency” (and I really encourage you to read that vision doc) is to simplify our code (eliminate Sendable noise, eliminate unnecessary and inefficient context switches). They’re asking us to be more thoughtful about whether the work being performed really calls for moving code off the main actor.

There are times that we definitely want to get work off the main actor. But any notions about “if it is asynchronous then it must be off the main actor” is at odds with trends in the language. It’s just not that simple.

[Respectfully, I’ll keep an eye out for your other questions on this forum, but will refrain from any further discussion on this thread unrelated to danmihai’s original question.]

Yes, this is why I started a new thread at Does I/O operation block its thread? . BTW I don't feel the need to continue this discussion at any forum thread, as I already received great explanations from @Jon_Shier there and from @itaiferber here.

Thanks! I finally got it, also sort of double watched videos from WWDC

1 Like