为什么 WWDC 谈话建议在主线程上使用 运行 非 UIKit 代码来修复数据竞争?

Why does WWDC talk suggest running non-UIKit code on main thread to fix data race?

我在看这个名为 Thread Sanitizer and Static Analysis 的 WWDC 演讲,演讲者向我们展示了如果两个不同的线程调用 notifyStartNetworkActivity:

就会出现数据竞争
var activityCount: Int = 0

public class ActivityCounter : NSObject {
    public func notifyStartNetworkActivity() {
        activityCount = activityCount + 1
        self.updateNetworkActivityUI()
    }
    
    func updateNetworkActivityUl() {
        WWDCJSONOperation.prepareNetworkActivity()
        if activityCount > 0 {
            WWDCJSONOperation.visibilityTimer?.invalidate()
            WWDCJSONOperation.visibilityTimer = nil
            UIApplication.shared().isNetworkActivityIndicatorVisible = true
        } else {
            /* To prevent the indicator from flickering on and off, we delay the hiding of the indicator by one second. This provides the chance to come in and invalidate the timer before it fires. */
            WWDCJSONOperation.visibilityTimer = Timer.scheduledTimer(timelnterval: 1.0, target: self, selector: #selector(ActivityCounter.fire(timer:)))
...

在 6:45,发言者接着说:

Now, I could have fixed this race by adding a lock. But notice that this is just a symptom. The next line here updates the UI. And we know that the UI updates should happen on the main thread. So the proper fix here is to dispatch both the counter increment and the UI update onto the main queue with Grand Central Dispatch. This will both take care of the logical problem in our application and also take care of the race because all the threads will access that count variable from the same thread.

public func notifyStartNetworkActivity() {
    DispatchQueue.main.async {
        activityCount = activityCount + 1
        self.updateNetworkActivityUI()
    }
}

我在这个修复中遇到的问题是我们在主线程中添加了不必要的工作。显然 UIKit 调用如 UIApplication.shared().isNetworkActivityIndicatorVisible = true 需要在主线程上完成,因为 UIKit 不是线程安全的。但是在主线程上完成了一些不必要的工作,例如更新 activityCount。正如我看过的其他 WWDC 演讲中所解释的那样,不必要的工作很糟糕,包括 Optimizing Your App for Multitasking on iPad in iOS 9:

So the most important thing you can do to keep your app responsive is to do as little work as possible on your main thread. The main thread's top priority is to respond to user events, and doing unnecessary work on your main thread means that the main thread has less time to respond to user events.

因此,在这种情况下,我会使用锁或 GCD 队列来控制访问。虽然这些增加了开销,但这种开销被添加到执行网络操作的后台线程中,因此我们可以使 UI 尽可能保持响应。然而,演讲者显然比我更了解多线程,所以我很好奇为什么在这种情况下演讲者说正确的解决方法是将非 UIKit 工作添加到主线程。

增加和更新这个模型的工作量 属性 是无关紧要的,并且考虑到演讲者无论如何都需要将 UI 更新分派到主线程,在那里也增加计数器,确实是最好的解决方案。

这是一个非常常见的场景,我们将模型和 UI 更新分派到主队列。只要您明智地限制分派给主线程的内容(以及频率),就应该没问题。但是由于我们必须在主线程上执行 UI 更新,无论如何,谨慎的做法是在主线程中进行简单的模型更新以消除任何数据竞争。

因此,执行计数器对象的简单递增绝对适合在主队列上执行,尤其是因为说话者无论如何都必须将 UI 更新分派到主线程。在如此简单的场景中引入另一种同步机制将是不必要的复杂化。