Block in sync method on background queue is executed on main thread - swift

Just started to learning about GCD and I am running into trouble because my code is still ran on the main thread while I created a background queue. This is my code:
import UIKit
class ViewController: UIViewController {
let queue = DispatchQueue(label: "internalqueue", qos: .background)
override func viewDidLoad() {
super.viewDidLoad()
dispatchFun {
assert(Thread.isMainThread)
let x = UIView()
}
}
func dispatchFun(handler: #escaping (() -> ())) {
queue.sync {
handler()
}
}
}
Surprising enough (for me), is that this code doesn't throw any error! I would expect the assertion would fail. I would expect the code is not ran on the main thread. In the debugger I see that when constructing the x instance, that I am in my queue on thread 1 (by seeing the label). Strange, because normally I see the main thread label on thread 1. Is my queue scheduled on the main thread (thread 1)?
When I change sync for async, the assertion fails. This is what I would expect to happen with sync aswell. Below is an attached image of the threads when the assertion failed. I would expect to see the exact same debug information when I use sync instead of async.
When reading the sync description in the Swift source, I read the following:
/// As an optimization, `sync(execute:)` invokes the work item on the thread which
/// submitted it, except when the queue is the main queue or
/// a queue targetting it.
Again: except when the queue is the main queue
Why does the sync method on a background dispatch queue cases the code to run on the main thread, but async doesn't? I can clearly read that the sync method on a queue shouldn't be ran on the main thread, but why does my code ignore that scenario?

I believe you’re misreading that comment in the header. It’s not a question of whether you’re dispatching from the main queue, but rather if you’re dispatching to the main queue.
So, here is the well known sync optimization where the dispatched block will run on the current thread:
let backgroundQueue = DispatchQueue(label: "internalqueue", attributes: .concurrent)
// We'll dispatch from main thread _to_ background queue
func dispatchingToBackgroundQueue() {
backgroundQueue.sync {
print(#function, "this sync will run on the current thread, namely the main thread; isMainThread =", Thread.isMainThread)
}
backgroundQueue.async {
print(#function, "but this async will run on the background queue's thread; isMainThread =", Thread.isMainThread)
}
}
When you use sync, you’re telling GCD “hey, have this thread wait until the other thread runs this block of code”. So, GCD is smart enough to figure out “well, if this thread is going to not do anything while I’m waiting for the block of code to run, I might as well run it here if I can, and save the costly context switch to another thread.”
But in the following scenario, we’re doing something on some background queue and want to dispatch it back to the main queue. In this case, GCD will not do the aforementioned optimization, but rather will always run the task dispatched to the main queue on the main queue:
// but this time, we'll dispatch from background queue _to_ the main queue
func dispatchingToTheMainQueue() {
backgroundQueue.async {
DispatchQueue.main.sync {
print(#function, "even though it’s sync, this will still run on the main thread; isMainThread =", Thread.isMainThread)
}
DispatchQueue.main.async {
print(#function, "needless to say, this async will run on the main thread; isMainThread =", Thread.isMainThread)
}
}
}
It does this because there are certain things that must run on the main queue (such as UI updates), and if you’re dispatching it to the main queue, it will always honor that request, and not try to do any optimization to avoid context switches.
Let’s consider a more practical example of the latter scenario.
func performRequest(_ url: URL) {
URLSession.shared.dataTask(with: url) { data, _, _ in
DispatchQueue.main.sync {
// we're guaranteed that this actually will run on the main thread
// even though we used `sync`
}
}
}
Now, generally we’d use async when dispatching back to the main queue, but the comment in the sync header documentation is just letting us know that this task dispatched back to the main queue using sync will actually run on the main queue, not on URLSession’s background queue as you might otherwise fear.

Let's consider:
/// As an optimization, `sync(execute:)` invokes the work item on the thread which
/// submitted it, except when the queue is the main queue or
/// a queue targetting it.
You're invoking sync() on your own queue. Is that queue the main queue or targeting the main queue? No, it's not. So, the exception isn't relevant and only this part is:
sync(execute:) invokes the work item on the thread which submitted it
So, the fact that your queue is a background queue doesn't matter. The block is executed by the thread where sync() was called, which is the main thread (which called viewDidLoad(), which called dispatchFun()).

Related

Swift Concurrency : Why Task is not executed on other background thread

I am trying to learn the swift concurrency but it brings in a lot of confusion. I understood that a Task {} is an asynchronous unit and will allow us to bridge the async function call from a synchronous context. And it is similar to DispatchQueue.Global() which in turn will execute the block on some arbitrary thread.
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
let data = try await asychronousApiCall()
print(data)
} catch {
print("Request failed with error: \(error)")
}
}
for i in 1...30000 {
print("Thread \(Thread.current)")
}
}
my asychronousApiCall function is below
func asychronousApiCall() async throws -> Data {
print("starting with asychronousApiCall")
print("Thread \(Thread.current)")
let url = URL(string: "https://www.stackoverflow.com")!
// Use the async variant of URLSession to fetch data
// Code might suspend here
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
When I try this implementation. I always see that "starting with asychronousApiCall" is printed after the for loop is done and the thread is MainThread.
like this
Thread <_NSMainThread: 0x600000f10500>{number = 1, name = main}
You said:
I understood that a Task {} is an asynchronous unit and will allow us to bridge the async function call from a synchronous context.
Yes.
You continue:
And it is similar to DispatchQueue.global() which in turn will execute the block on some arbitrary thread.
No, if you call it from the main actor, it is more akin to DispatchQueue.main.async { … }. As the documentation says, it “[r]uns the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor” [emphasis added]. I.e., if you are currently on the main actor, the task will be run on behalf of the main actor, too.
While it is a probably mistake to dwell on direct GCD-to-concurrency mappings, Task.detached { … } is more comparable to DispatchQueue.global().async { … }.
You commented:
Please scroll to figure 8 in last of the article. It has a normal task and Thread is print is some other thread.
figure 8
In that screen snapshot, they are showing that prior to the suspension point (i.e., before the await) it was on the main thread (which makes sense, because it is running it on behalf of the same actor). But they are also highlighting that after the suspension point, it was on another thread (which might seem counterintuitive, but it is what can happen after a suspension point). This is very common behavior in Swift concurrency, though it can vary.
FWIW, in your example above, you only examine the thread before the suspension point and not after. The take-home message of figure 8 is that the thread used after the suspension point may not be the same one used before the suspension point.
If you are interested in learning more about some of these implementation details, I might suggest watching WWDC 2021 video Swift concurrency: Behind the scenes.
While it is interesting to look at Thread.current, it should be noted that Apple is trying to wean us off of this practice. E.g., in Swift 5.7, if we look at Thread.current from an asynchronous context, we get a warning:
Class property 'current' is unavailable from asynchronous contexts; Thread.current cannot be used from async contexts.; this is an error in Swift 6
The whole idea of Swift concurrency is that we stop thinking in terms of threads and we instead let Swift concurrency choose the appropriate thread on our behalf (which cleverly avoids costly context switches where it can; sometimes resulting code that runs on threads other than what we might otherwise expect).

Swift Async - what thread does Task.detached run on?

I've written a global function that allows me to synchronously wait for the completion of an async task (for experimentation purposes only!) with the help of a DispatchSemaphore.
///
/// synchronously waits for the completion of an asynchronous closure
/// - parameter handler: the task to run, asynchronously
/// - note: don't use this in production code, it violates
/// the Swift runtime contract of "forward" progress, it's never
/// safe to block a thread and wait for another thread to unblock it
/// (on a single core system, this may hang forever)
///
func sync(handler: #escaping () async -> Void) {
let sema = DispatchSemaphore(value: 0)
// default `Task.Priority` to `asyncDetached` is `nil`
Task.detached {
await handler()
sema.signal()
}
// blocks the current thread, waiting for the async Task to finish
sema.wait()
}
I noticed that if I just use an Task block to launch the asynchronous tasks a deadlock arises, presumably because sema.wait() is blocking the current thread, preventing the Task block from ever running (as normal Task blocks seem to inherit the current thread), so it will just wait forever.
Using Task.detached does not seem to block the thread, making the above code work.
This appears to be related to the Task.Priority? to which the detached call is assigned.
The default parameter value is to which is nil when calling Task.detached.
This can be customized to a different priority level, indicating the dispatch QoS.
My questions therefore is: what thread does Task.Priority? = nil relate to? It does not appear to ever be the same thread as where it was launched from. Does Task.Priority? = nil indicate to the Swift runtime to always run on a different thread?
EDIT
Follow on question - how do Task and Task.detached differ in regard to threading?
The priority does not influence the priority on your app's side. It is a hint for the host to response to several requests in a priority. The host can completely ignore that.
refer to this answer for more information.
Therefore task priority has nothing to do with the thread with that aside.
you'r task is getting dispatched every time and Task.Priority refers to no thread and has nothing to do with threads.
Rather than using semaphores and asyncDetached, I suggest using GCD queues and DispatchGroups. They are lightweight and very easy to use. You just
Create a DispatchGroup (let group = DispatchGroup(), call group.notify(queue:work:) to submit a block to be run when the group of tasks is complete.
Call group.enter() each time you begin a task you want to wait for,
Call group.leave() when you complete a task
The system will then invoke the block you submitted in your notify(queue:work:) call once the tasks are complete. That's all there is to it. You can submit 0, one, or many tasks. If you haven't submitted any, or they have all completed, your notify closure fires immediately. If some tasks are still running, your notify closure isn't called until your tasks call group.leave()

What exactly is GCD overcommit and is it dangerous

I've been working on some code recently where I want to ensure that certain tasks run sequentially always execute on the same thread.
As an experiment to get a feel for how this would work I created my own thread using this example Thread Example
My code works fine in that I call a method on the global queue then I enqueue an operation on my Custom Thread.
func currentQueueName() -> String? {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8)
}
DispatchQueue.global().async {
print(self.currentQueueName())
myThread.enqueue {
print(self.currentQueueName())
//prints "com.apple.root.default-qos.overcommit"
for i in 1...100 {
print(i)
}
}
}
Whenever I print out the current thread during my Custom Thread Execution the thread name says.
("com.apple.root.default-qos.overcommit")
I don't get any errors or crashes.
1) What exactly does this over commit mean?
2) How have I caused it by using my own thread?
3) Is it dangerous to be seeing this message in production code?
4) If it is dangerous how can I use my Custom Thread safely
Update
After reading a post on Swift forum I'm beginning to think that over commit queue refers to any thread that isn't from Dispatch Queue Global.
I'm still not 100% certain though.

Using completion handler together with DispatchQueue

I have learned that concurrent DispatchQueue allows the code inside it to return immediately, hence not blocking the calling thread. This is usually used with background tasks loading large data.
I have also learned that completion handler (for example, in URLSession ) allows the code inside handler to be executed after some task finishes.
My question is: does it mean that concurrent dispatch queue and completion handler have overlapping purpose? If I already used completion handler, there is no need to wrap it with a concurrent dispatch queue?
For example, below is a time-consuming data loading task using URLSession, is it a good idea to wrap it with a concurrent dispatch queue?
URLSession(configuration: URLSessionConfiguration.default).dataTask(with: propertiesRequest) { data, response, error in
// print("within dataTask: data: \(data), response: \(response), error: \(error)")
if let error = error {
print(error)
} else if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
print("success: property task request")
do {
handler(responseDict, nil) // This is a function supplied by caller
} catch let error as NSError {
handler(nil, error)
}
}
}
}
You don't have to use Grand Central Dispatch (GCD) dispatch queues in conjunction with time-consuming URLSession request.
You'd might use GCD inside the dataTask completion handler closure if:
If you are doing something inside the closure is, itself, very time consuming (e.g. processing very complicated request) and you don't want to tie up the serial operation queue that URLSession uses for processing its completion handlers (and delegate method). This does not appear to be the issue here (e.g. parsing JSON responses is generally quick enough we don't have to worry about this), but just FYI. Or,
If, when you're done parsing the response of dataTask, if you want to update some model object or update the UI. You want to do those on the main queue.
For example, if your request was returning a bunch of objects to show in some tableview, you'd dispatch the update of the model and the UI to the main queue:
DispatchQueue.main.async {
self.objects = ...
self.tableView.reloadData()
}
But you don't have to worry about the time-consuming URLSession request, itself. That already happens asynchronously, so you don't have to dispatch that to a background queue.
The DispatchQueue and the completion handler do not overlap but rather can be used as a seamless solution for handling queues. The data loading task in URLSession is already asynchronous and thus have no need to be wrapped in a DispatchQueue.
DispatchQueue - assigning tasks to a specific thread for better performance (global queue) / user experience (main queue).
Completion Handlers - guarantees certain code will run when the task has been completed. However, the execution is on the current thread unless explicitly stated otherwise.
For example, calling on method A which employs DispatchQueue.global.async will queue the task on the global queue, freeing the main queue for more important (UI) tasks. After some time, the task would have been completed and usually, we would want to do something with the data. If it is UI related, we definitely want to call on DispatchQueue.main.async to update the screen with info or if it is trivial, no calls need to be made and vanilla code would suffice.

Swift: synchronously perform code in background; queue.sync does not work as I would expect

I would like to perform some code synchronously in the background, I really thought this is the way to go:
let queue = DispatchQueue.global(qos: .default)
queue.async {
print("\(Thread.isMainThread)")
}
but this prints true unless I use queue.async. async isn't possible as then the code will be executed in parallel. How can I achieve running multiple blocks synchronously in the background?
What I would like to achieve: synchronize events in my app with the devices calendar, which happens in the background. The method which does this can be called from different places multiple times so I would like to keep this in order and in the background.
Async execution isn't your problem, since you only care about the order of execution of your code blocks relative to each other but not relative to the main thread. You shouldn't block the main thread, which is in fact DispatchQueue.main and not DispatchQueue.global.
What you should do is execute your code on a serial queue asynchronously, so you don't block the main thread, but you still ensure that your code blocks execute sequentially.
You can achieve this using the following piece of code:
let serialQueue = DispatchQueue(label: "serialQueue")
serialQueue.async{ //call this whenever you need to add a new work item to your queue
//call function here
}
DispatchQueue is not equal to a Thread. Think of it as of a kind of abstraction over the thread pool.
That being said, main queue is indeed "fixed" on the main thread. And that is why, when you synchronously dispatch a work item from the main queue, you are still on the main thread.
To actually execute sync code in the background, you have to already be in the background:
DispatchQueue.global().async {
DispatchQueue.global().sync {
print("\(Thread.isMainThread)")
}
}
This will print false.
Also, as user #rmaddy correctly pointed out in comments, doing any expensive tasks synchronously from the main queue might result in your program becoming unresponsive, since the main thread is responsible for the UI updates.