在主线程上执行后台队列中的同步块阻止方法

时间:2019-03-05 19:16:03

标签: swift multithreading grand-central-dispatch

刚开始学习GCD时,我就遇到了麻烦,因为在创建后台队列时,我的代码仍在主线程上运行。这是我的代码:

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

(对我而言)令人惊讶的是,这段代码没有引发任何错误!我希望断言会失败。我希望代码不会在主线程上运行。在调试器中,我看到在构造x实例时,我在线程1上的队列中(通过查看标签)。奇怪,因为通常我在线程1上看到主线程标签。我的队列是否安排在主线程(线程1)上?

当我将sync更改为async时,断言失败。我也希望sync也会发生这种情况。下面是断言失败时线程的附加图像。 当我使用sync而不是async时,希望能看到完全相同的调试信息。

enter image description here

在Swift来源中阅读sync描述时,我读到以下内容:

/// 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.

再次:除非队列是主队列

为什么后台调度队列上的sync方法将代码放在主线程上运行,而async却没有?我可以清楚地了解到,不应在主线程上运行队列上的sync方法,但是为什么我的代码会忽略这种情况?

2 个答案:

答案 0 :(得分:2)

让我们考虑一下

/// 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.

您要在自己的队列中调用sync()。该队列是主队列还是以主队列为目标?不,这不对。因此,该异常无关紧要,只有这一部分是:

  

sync(execute:)在提交它的线程上调用工作项

因此,您的queue是后台队列这一事实无关紧要。该块由调用sync()的线程(即主线程(​​称为viewDidLoad(),称为dispatchFun())执行。

答案 1 :(得分:2)

我相信您在标题中误读了该评论。这不是您是否要从main队列调度 的问题,而是您是否要向main队列调度的问题。 / p>

因此,这是众所周知的sync优化,其中分派的块将在当前线程上运行:

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)
    }
}

使用sync时,您在告诉GCD“嘿,让该线程等待,直到另一个线程运行此代码块”。因此,GCD非常聪明,可以找出“好吧,如果在等待代码块运行时该线程不做任何事情,那么我也可以在这里运行它,并节省昂贵的上下文切换到另一个线程。”

但是在以下情况下,我们正在某些后台队列上执行某些操作,并希望将其分派回main队列。在这种情况下,GCD将不会执行上述优化,而是将始终运行在main队列上分配给main队列的任务:

// 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)
        }
    }
}

之所以这样做,是因为某些事情必须在主队列上运行(例如UI更新),并且如果您将其分派到主队列,它将始终满足该请求,而不尝试执行任何操作优化以避免上下文切换。


让我们考虑一下后一种情况的更实际的例子。

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`
        }
    }
}

现在,通常我们在分派回主队列时会使用async,但是sync标头文档中的注释只是让我们知道此任务是使用以下方法分派回主队列的sync实际上将在主队列上运行,而不是您可能担心的在URLSession的后台队列上运行。