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でもいくつか実装例があります。
タイムアウトの設定
DispatchSemaphore
のwait
にはタイムアウトを指定することができます。
タイムアウトを指定することで、後続の処理が結果待ちの状態で止まってしまうのを防ぐことができます。
switch semaphore.wait(timeout: .now() + 2.0) { case .success: // 時間内に結果が帰ってきた時 case .timedOut: // 時間切れの時 }
この場合、返り値がDispatchTimeoutResult
になるので、このenum
を見ることで時間切れかどうか確認できます。
(システム時間を使う、wait(wallTimeout: DispatchWallTime)
もあります)