Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

enter image description here

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?

like image 750
J. Doe Avatar asked Mar 05 '19 19:03

J. Doe


People also ask

What are async/background threads?

Async/Background Threads If you want to make web requests in C#, or just want to do some background processing, you’ll need to use asynchronous background tasks to not block up the main thread. We’ll discuss what they are, and how to use them. What Is Async/Await? To use Tasks, you must first understand the concept of async / await.

What is the difference between blocking and asynchronously executed threads?

When an operation holds up everything else on the thread, this is called blocking. When a thread executes asynchronously, elements begin running one after the other without waiting for the previous operation to complete.

What is the difference between synchrony and blocking in Java?

When a thread executes synchronously, that means it waits for each operation on the thread to complete before running the next one. Everything else on that thread is blocked until the current operation finishes. When an operation holds up everything else on the thread, this is called blocking.

What is a background task queue in a service?

A background task queue is based on the .NET 4.x QueueBackgroundWorkItem: The BackgroundProcessing method returns a Task, which is awaited in ExecuteAsync. Background tasks in the queue are dequeued and executed in BackgroundProcessing. Work items are awaited before the service stops in StopAsync.


2 Answers

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.

like image 192
Rob Avatar answered Jun 09 '23 18:06

Rob


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()).

like image 26
Ken Thomases Avatar answered Jun 09 '23 18:06

Ken Thomases