担当していた案件で、Xcodeをバージョンアップしてから、以下のようなワーニングが表示されるようになった。
iOS 15.5 の Simulator ではワーニングは表示されず、 iOS 16.1 の Simulator を使うとワーニングが表示された。
Thread running at QOS_CLASS_USER_INITIATED waiting on a lower QoS thread running at QOS_CLASS_DEFAULT. Investigate ways to avoid priority inversions
該当する部分のコードを要約すると以下のような感じ。
final class HogeAPI {
private var workItem: DispatchWorkItem?
func fetchHoge() {
workItem?.cancel()
workItem = DispatchWorkItem(block: { [weak self] in
guard let self = self else {
return
}
let semaphore = DispatchSemaphore(value: 0)
let translateWorkItem = DispatchWorkItem { [weak self] in
/// 省略
}
DispatchQueue.global().async { <--- この async 内が QOS_CLASS_USER_INITIATED で実行される
self.apiGateway.fetchFoo {
/// 省略
semaphore.signal() <--- これが QOS_CLASS_DEFAULT で実行される
}
semaphore.wait() <--- ここでワーニングが表示された
guard !translateWorkItem.isCancelled else {
return
}
self.apiGateway.fetchBar {
/// 省略
semaphore.signal()
}
semaphore.wait()
DispatchQueue.main.async(execute: translateWorkItem)
}
})
DispatchQueue.main.async(execute: workItem!)
}
}
ワーニングが表示されたのは、1回目のsemaphore.wait()
の部分。
で、調査を開始。
調べてみると、公式サイトにも言及があり、Diagnose and resolve priority inversions で解決方法が説明されている。
dispatch_semaphore_wait
や dispatch_group_wait
を使う場合、QoS
の不一致があると、優先順位の逆転の影響を受けやすくなるので、予防措置を取ってね(You can take these precautions to avoid priority inversions in your code
)っていうことだった。
今回のケースは dispatch_semaphore_wait
の優先順位が逆転してしまっている事(待機中のスレッドの QoS が、 Signal スレッドの QoS よりも優先度が上になってしまっている事)が原因と思われる。
ただ、ここで気になったのは、以下の点。
-
DispatchQueue.global().async
内がQOS_CLASS_USER_INITIATED
で実行されている -
semaphore.signal()
内がQOS_CLASS_DEFAULT
で実行されている
DispatchQueue.global().async
内って、 QOS_CLASS_DEFAULT
じゃないの? って思っていたので、ちょっと混乱してしまった。
確認の為に、以下のようなログを追加して動かしてみた。
print("🍏 X", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
final class HogeAPI {
private var workItem: DispatchWorkItem?
func fetchHoge() {
workItem?.cancel()
print("🍏 1", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
workItem = DispatchWorkItem(block: { [weak self] in
guard let self = self else {
return
}
print("🍏 2", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
let semaphore = DispatchSemaphore(value: 0)
let translateWorkItem = DispatchWorkItem { [weak self] in
print("🍏 7", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
/// 省略
}
DispatchQueue.global().async { <--- この async 内が QOS_CLASS_USER_INITIATED で実行される
print("🍏 3", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
self.apiGateway.fetchFoo {
print("🍏 4", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
/// 省略
semaphore.signal() <--- これが QOS_CLASS_DEFAULT で実行される
}
semaphore.wait() <--- ここでワーニングが表示された
print("🍏 5", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
guard !translateWorkItem.isCancelled else {
return
}
self.apiGateway.fetchBar {
print("🍏 6", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
/// 省略
semaphore.signal()
}
semaphore.wait()
DispatchQueue.main.async(execute: translateWorkItem)
}
})
DispatchQueue.main.async(execute: workItem!)
}
}
結果はこんな感じ。
以下が、 QualityOfService
の値なので、🍏 3
も🍏 5
も見事に userInitiated
になっていた…
@available(iOS 8.0, *)
public enum QualityOfService : Int, @unchecked Sendable {
case userInteractive = 33
case userInitiated = 25
case utility = 17
case background = 9
case `default` = -1
}
ということで、解決方法は、おそらく2つあると考えられる。
-
semaphore.wait()
側の優先度を下げる -
semaphore.signal()
側の優先度を上げる
で、実際に試してみた。
解決方法1(semaphore.wait()
側の優先度を下げる)
DispatchQueue.global().async
部分に、明示的に低い優先度を指定して実行してみた。
DispatchQueue.global(qos: .utility).async {
結果は、ワーニングは表示されず、🍏 3
も🍏 5
も優先度が utility
で実行された。
解決方法2(semaphore.signal()
側の優先度を上げる)
semaphore.signal()
の実行の優先度を上げて実行してみた。
DispatchQueue.global(qos: .userInitiated).async {
print("🍏 4+", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
semaphore.signal()
}
結果は、ワーニングは表示されず、 semaphore.signal()
の実行は、 userInitiated
で実行されていることが確認できた。
最終的に
今回の件は、APIのレスポンスを元にUIの更新処理が発生するので、「解決方法2(semaphore.signal()
側の優先度を上げる)」の方法で、 Apple のドキュメントのように Queue の定義を行い対応した。
内容としては、以下を追加して、
let userInitiatedQueue = DispatchQueue(label: "userInitiated_queue", qos: .userInitiated)
semaphore.signal()
部分を追加した Queue で実行するように修正。
userInitiatedQueue.async {
semaphore.signal()
}
私みたいに、 DispatchQueue.global().async
で問題に遭遇した方の参考になれば幸いです。