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

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

刚开始学习 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 的队列中(通过查看标签)。 st运行ge,因为一般我在线程1上看到的是主线程标签,请问我的队列是在主线程(线程1)上调度的吗?

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

阅读 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 不会?我可以清楚地看到队列上的同步方法不应该在主线程上使用 运行,但为什么我的代码忽略了这种情况?

让我们考虑一下:

/// 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:) invokes the work item on the thread which submitted it

因此,您的 queue 是后台队列这一事实并不重要。该块由调用sync()的线程执行,该线程是主线程(调用viewDidLoad(),调用dispatchFun())。

我认为您误读了 header 中的评论。这不是你是否从 发送 main queue 的问题,而是你是否正在发送 的问题] main queue.

因此,这是众所周知的 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 足够聪明,可以弄清楚“好吧,如果这个线程在我等待代码块 运行 时不做任何事情,我不妨在这里 运行 如果我可以,并将代价高昂的上下文切换保存到另一个线程。”

但在下面的场景中,我们在某些背景 queue 上做一些事情,并希望将其分派回 main queue。在这种情况下,GCD 不会进行上述优化,而是始终 运行 将任务分派到 main queue 上 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)
        }
    }
}

它这样做是因为某些事情必须 运行 在主 queue 上(例如 UI 更新),如果你要将它分派到主 queue,它将始终尊重该请求,并且不会尝试进行任何优化以避免上下文切换。


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

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

现在,通常我们会在分派回主 queue 时使用 async,但是 sync header 文档中的注释只是让我们知道使用 sync 分派回主 queue 的任务实际上 运行 在主 queue 上,而不是在 URLSession 的后台 queue正如您可能担心的那样。