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

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

該当する部分のコードを要約すると以下のような感じ。

```swift
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!)
    }
}
```

ワーニングが表示されたのは、１回目の`semaphore.wait()`の部分。
で、調査を開始。

調べてみると、公式サイトにも言及があり、**Diagnose and resolve priority inversions** で解決方法が説明されている。

https://developer.apple.com/documentation/xcode/diagnosing-performance-issues-early

`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` じゃないの？ って思っていたので、ちょっと混乱してしまった。

確認の為に、以下のようなログを追加して動かしてみた。

```swift
print("🍏 X", Thread.isMainThread, Thread.current, Thread.current.qualityOfService.rawValue)
```
```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30834/1674911c-ea70-5728-3dc9-daa35802ec84.png)
以下が、　`QualityOfService` の値なので、`🍏 ３`も`🍏 ５`も見事に `userInitiated` になっていた…

```swift
@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
}
```

ということで、解決方法は、おそらく２つあると考えられる。

- `semaphore.wait()` 側の優先度を下げる
- `semaphore.signal()` 側の優先度を上げる


で、実際に試してみた。

### 解決方法１（`semaphore.wait()` 側の優先度を下げる）

`DispatchQueue.global().async` 部分に、明示的に低い優先度を指定して実行してみた。

```swift
DispatchQueue.global(qos: .utility).async {
```

結果は、ワーニングは表示されず、`🍏 ３`も`🍏 ５`も優先度が `utility` で実行された。
![スクリーンショット 2023-03-06 17.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30834/e8f44b85-8c77-fa8e-7ca4-40004ea5f266.png)

### 解決方法２（`semaphore.signal()` 側の優先度を上げる）

`semaphore.signal()` の実行の優先度を上げて実行してみた。

```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30834/4c00e42d-5cae-8e21-4f22-18c51fa9a678.png)

## 最終的に

今回の件は、APIのレスポンスを元にUIの更新処理が発生するので、「解決方法２（`semaphore.signal()` 側の優先度を上げる）」の方法で、 Apple のドキュメントのように Queue の定義を行い対応した。


内容としては、以下を追加して、

```swift
let userInitiatedQueue = DispatchQueue(label: "userInitiated_queue", qos: .userInitiated)
```

`semaphore.signal()` 部分を追加した Queue で実行するように修正。

```swift
userInitiatedQueue.async {
    semaphore.signal()
}
```



私みたいに、 `DispatchQueue.global().async` で問題に遭遇した方の参考になれば幸いです。