2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

「Thread running at QOS_CLASS_USER_INITIATED waiting on a lower QoS thread running at QOS_CLASS_DEFAULT. Investigate ways to avoid priority inversions」と怒られた原因と対応

Posted at

担当していた案件で、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_waitdispatch_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!)
    }
}

結果はこんな感じ。
スクリーンショット 2023-03-06 17.49.31.png
以下が、 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 で実行された。
スクリーンショット 2023-03-06 17.52.21.png

解決方法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 で実行されていることが確認できた。
スクリーンショット 2023-03-06 17.55.51.png

最終的に

今回の件は、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 で問題に遭遇した方の参考になれば幸いです。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?