如何确保我的 DispatchQueue 专门在主线程上执行一些代码?

How do I ensure my DispatchQueue executes some code on the main thread specifically?

我有一个管理数组的单例。这个单例可以从多个线程访问,所以它有自己的内部 DispatchQueue 来管理 read/write 跨线程访问。为简单起见,我们将其称为串行队列。

有时单例将从数组中读取并更新 UI。我该如何处理?

不知道我的内部调度队列是哪个线程,对吧?这只是一个我不用担心的实现细节?在大多数情况下这似乎很好,但在这个特定函数中我需要确保它使用主线程。

是否可以按照以下方式做某事:

myDispatchQueue.sync { // Synchronize with internal queue to ensure no writes/reads happen at the same time
    DispatchQueue.main.async { // Ensure that it's executed on the main thread
        for item in internalArray {
            // Pretend internalArray is an array of strings
            someLabel.text = item
        }
    }
}

所以我的问题是:

  1. 可以吗?似乎 weird/wrong 是嵌套调度队列。有没有更好的办法?也许像 myDispatchQueue.sync(forceMainThread: true) { ... }?
  2. 如果我没有使用 DispatchQueue.main.async { ... },并且我从主线程调用该函数,我可以确定我的内部调度队列将在调用它的同一(主)线程上执行它吗?或者这也是一个 "implementation detail" 它可能在的地方,但它也可以在后台线程上调用?

基本上我很困惑,线程似乎是一个你不应该担心队列的实现细节,但是当你确实需要担心的时候会发生什么?

简单示例代码:

class LabelUpdater {
    static let shared = LabelUpdater()

    var strings: [String] = []
    private let dispatchQueue: dispatchQueue

    private init {
        dispatchQueue = DispatchQueue(label: "com.sample.me.LabelUpdaterQueue")
        super.init()
    }

    func add(string: String) {
        dispatchQueue.sync {
            strings.append(string)
        }
    }

    // Assume for sake of example that `labels` is always same array length as `strings`
    func updateLabels(_ labels: [UILabel]) {
        // Execute in the queue so that no read/write can occur at the same time.
        dispatchQueue.sync {
            // How do I know this will be on the main thread? Can I ensure it?
            for (index, label) in labels.enumerated() {
                label.text = strings[index]
            }
        }
    }
}

是的,您可以将对一个队列的调度嵌套在对另一个队列的调度中。我们经常这样做。

但是要非常小心。仅使用来自同步队列的分派将异步分派包装到主队列是不够的。您的第一个示例不是线程安全的。您从主线程访问的那个数组可能正在从您的同步队列中发生变化:

这是一个 race condition 因为您可能有多个线程(同步队列的线程和主线程)与同一个集合交互。与其让你的分派到主队列的块直接交互 objects,你应该复制它,这就是你在分派到主队列中引用的内容。

例如,您可能想要执行以下操作:

func process(completion: @escaping (String) -> Void) {
    syncQueue.sync {
        let result = ...            // note, this runs on thread associated with `syncQueue` ...

        DispatchQueue.main.async {
            completion(result)      // ... but this runs on the main thread
        }
    }
}

这确保主队列不与此 class 的任何内部属性交互,而只是在传递给 syncQueue 的此闭包中创建的 result


请注意,所有这些都与它是一个单身人士无关。但是既然你提出了这个话题,我建议不要对模型数据使用单例。它适用于接收器、无状态控制器等,但通常不建议用于模型数据。

我绝对不鼓励直接从单例启动 UI 控件更新的做法。我倾向于提供这些方法完成处理程序闭包,并让调用者负责产生的 UI 更新。当然,如果你想将闭包分派到主队列(为了方便,在许多第三方 API 中很常见),那很好。但是单身人士不应该进入并更新 UI 控制自己。

我假设您所做的所有这些只是为了说明目的,但我向可能不会理解这些担忧的未来读者添加了这个警告词。

尝试使用 OperationQueues(Operations),因为它们确实有状态:

  • isReady: 准备开始
  • isExecuting: 任务当前运行
  • isFinished: 一旦进程完成
  • isCancelled: 任务已取消

操作队列的好处:

  • 确定执行顺序
  • 观察他们的状态
  • 正在取消操作

Operations can be paused, resumed, and cancelled. Once you dispatch a task using Grand Central Dispatch, you no longer have control or insight into the execution of that task. The NSOperation API is more flexible in that respect, giving the developer control over the operation’s life cycle

https://developer.apple.com/documentation/foundation/operationqueue

https://medium.com/@aliakhtar_16369/concurrency-in-swift-operations-and-operation-queue-part-3-a108fbe27d61