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



我在看一个名为"线程清理器和静态分析"的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分,演讲者说:

现在,我本可以通过添加一个锁来修复这场比赛。但请注意这只是一个症状。此处的下一行更新UI。我们知道UI更新应该发生在主线程上。所以正确修复方法是同时调度计数器增量和UI更新使用Grand Central Dispatch进入主队列。这两者都需要注意我们应用程序中的逻辑问题,同时也注意因为所有线程都将从相同的线程。

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

这个修复程序的问题是,我们向主线程添加了不必要的工作。显然,像UIApplication.shared().isNetworkActivityIndicatorVisible = true这样的UIKit调用需要在主线程上完成,因为UIKit不是线程安全的。但是在主线程上做了一些不必要的工作,比如更新activityCount。正如我看过的其他WWDC演讲中所解释的那样,不必要的工作是糟糕的,包括在iOS 9中优化iPad上的多任务应用程序:

因此,保持应用程序响应最重要的是在主线程上尽可能少地执行工作。主要线程的首要任务是响应用户事件主线程上不必要的工作意味着主线程响应用户事件的时间更短。

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

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

这是一个非常常见的场景,我们将模型和UI更新都发送到主队列。只要您明智地限制向主线程发送的内容(以及频率(,您就应该没事。但是,由于我们必须在主线程上执行UI更新,无论如何,在那里包括琐碎的模型更新以消除任何数据竞争也是谨慎的。

因此,执行计数器对象的简单增量绝对适合在主队列上执行,尤其是因为演讲者无论如何都必须将UI更新调度到主线程。在这样一个简单的场景中引入另一种同步机制将是不必要的复杂性。