しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

DispatchSemaphoreで非同期処理の完了を待つ

Swiftにasync/awaitがなかなかこないので書いてみました。
コールバック地獄が嫌いな人向けの記事です。

DispatchSemaphoreの使い方

なんらかの大人の事情で、非同期処理を同期的なメソッドと同じような書き方で処理したい場合があると思います。 (もちろん良いとは言えないですが) 例えば、completion handlerで順番に処理したいことが多くなり、JSのcallback hellのように、ネストが深くなってしまう場合です。

そのような場合に、DispatchSemaphoreを活用することで、処理の記述を見やすくすることができます。

https://developer.apple.com/documentation/dispatch/dispatchsemaphoredeveloper.apple.com

実装は至ってシンプルで、DispatchSemaphoreは内部でカウントを持ち、waitでデクリメントをして、カウントが0以上になるまでブロックします。
signalでインクリメントをするので、処理が完了した際にsignalを呼ぶという使い方になります。

基本の構造

非同期処理を行い、完了後completionを呼ぶ形のメソッドに対しては、このような形で使うことで、semaphore.wait()の部分で処理をブロックすることができます。

let semaphore = DispatchSemaphore(value: 0)

// 非同期処理を行うメソッド.
doSomething(completion: {
    semaphore.signal()
})
// signalが呼ばれるまで待つ.
semaphore.wait()

ただし、メインスレッドで、非同期処理を実行するメソッドに対してこのような書き方をすると、そのままメインスレッドをブロックしてしまうので注意が必要です。

たとえば、

func doSomething(completion: @escaping (Data?) -> Void)) {
    // ...
    completion(data)
}

のようなメソッドに対しては、

func doSomethingAwait() -> Data? {
    let semaphore = DispatchSemaphore(value: 0)
    var result: Data?

    doSomething(completion: { data in
        result = data
        semaphore.signal()
    })
    semaphore.wait()

    return result
}

とすれば、同期的なメソッドと同じような書き方ができます。

これを、DispatchQueue.asyncなどを使ってサブスレッドで動かせば、複数のメソッドをネストすることなく記述することができます。

DispatchQueue.global(qos: .userInitiated).async {
    let dataA = doSomethingAAwait()
    let dataB = doSomethingBAwait(dataA)
    // ...
}

これを、うまくメソッドチェーンで書けるように工夫すれば、他言語のPromiseのようなインターフェースが実現できます。
OSSでもいくつか実装例があります。

github.com

タイムアウトの設定

DispatchSemaphorewaitにはタイムアウトを指定することができます。
タイムアウトを指定することで、後続の処理が結果待ちの状態で止まってしまうのを防ぐことができます。

switch semaphore.wait(timeout: .now() + 2.0) {
case .success:
    // 時間内に結果が帰ってきた時
case .timedOut:
    // 時間切れの時
}

この場合、返り値がDispatchTimeoutResultになるので、このenumを見ることで時間切れかどうか確認できます。

(システム時間を使う、wait(wallTimeout: DispatchWallTime)もあります)