优先级反转是一个出乎意料的多线程任务调度状态,往往出现于高优先级任务等待低优先级释放临界资源。同时,该次优先级任务被次高优先级任务抢占打断执行,导致次高优先级任务先于高优先级任务执行。优先级反转带来的程序问题可大可小,因为任务的延迟执行很难被察觉(火星探测器上的那次绝对是最值钱的一次,🤣),如下文中提到的这个🌰。

时灵时不灵的 Thread Performance Checker

Xcode 在 Debug 环境下提供了一个「Thread Performance Checker」,可以帮助开发者诊断多线程性能问题。不过如果以遇到的这个问题来看的话,它可能需要调试机的系统提供一些服务支持。示例代码如下:

1
2
3
DispatchQueue(label: "X", qos: .userInitiated).async {
let data = try Data(contentsOf: url)
}

在具体说明问题之前,先补充说明一下。Data 的 contentsOf 初始化方法除了可以直接读取本地文件 URL 的数据以外,还可以请求 URL 的数据。如果你直接在主线程中使用后者,你会收到下面这个日志提示:

Synchronous URL loading of {url} should not occur on this application’s main thread as it may lead to UI unresponsiveness. Please switch to an asynchronous networking API such as URLSession.

结合优先级反转问题本身的提示,我们可以得知 Data(contentsOf: remoteURL) 发生了什么:

Thread Performance Checker: Thread running at User-initiated quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions.

即开启一个 qos 为 default 的队列同步执行 remoteURL 的数据请求,这也是导致优先级反转的根本原因。奇怪的地方在于 iOS 16.7.7 上 Xcode 并没能诊断出此问题,而在 iOS 17.3 上将它暴露了出来。问题的解决方式也很容易,当 url 不是 fileURL 时,自行开启异步下载任务。

总结

老实说,从一开始并没有留意到 contentsOf 会自动请求 URL 数据的特性。如果从业务角度“狡辩”,在写下这段代码时并不会存在 remoteURL 的这一可能性。而随着业务本身的迭代,在某个版本后支持了这个选项,从功能验收来看也没问题就一直忽略了这个 API 的使用陷阱。但是从逻辑的完整性来看,remoteURL 的处理本就应该是不可或缺的一步。任重道远,继续修炼~