DispatchSemaphoreを使って、非同期を同期的に待ち合わせる
Table of Contents
この記事で紹介している方法は、DispatchSemaphoreを使用したものです。 セマフォは非同期処理や並列処理をするときの、同期や排他制御を実現する方法のひとつです。
非同期処理が連続するときに、ネストを深くしたくない。でも、ライブラリは使いたくない。そんなときに便利な実装方法です。今回は、DispatchSemaphore
の使い方をまとめます。
※ サンプルコードは、実際の案件で使用したコードを書き換えているため、正しく動作しない可能性があります。
課題感 #
iOSのアプリ開発をしていると、非同期処理は不可避。さらにはコールバックのネスト地獄になることさえあります。「あの非同期処理が正常に終わったら、次にあれをやって……」のような処理が続くと、ネストが深くなってしまいます。
このような非同期処理の待ち合わせにはPromiseのライブラリが便利ですが、使いたくない場合もあります。例えば、非同期処理がそれほど多くない場合、部分的に使用したい場合などです。PromiseKit・promises・Hydraといった便利なライブラリがすでにありますが、わざわざ入れるほどでもない場合があるということですね。
そこで、DispatchSemaphore
の出番です。
DispatchSemaphore
とは #
DispatchSemaphore - Dispatch | Apple Developer Documentation
DispatchSemaphoreは、いわゆるカウンティングセマフォと呼ばれるものです。カウンターが負の状態のときは後続の処理をブロックし、0のときにブロックを開放して処理を再開します。セマフォに対して wait()
を実行するとカウンターをデクリメントし、signal()
を呼ぶとカウンターはインクリメントされます。
適用事例 #
私の場合、FirebaseのStorageへ複数のファイルをアップロードする際に、この問題に遭遇しました。
DispatchSemaphoreで書く前 #
Firebaseの場合、一つのファイルをアップロードするごとに、StorageUploadTask
のインスタンスが生成されます。それらの実行結果を処理する方法は2つ。StorageUploadTaskにコールバックを渡すか、タスクの.observe(:StorageTaskStatus)
を通して、成功や失敗などの各ステータスごとに監視とコールバックを書く必要があります。
極端な例だと以下のようになります。
コールバックの場合。
// 事前にProgress Indicatorを表示しておく
indicator.show()
// アップロード
let uploadTask = fileReference.putData(data, metadata: metaData) { (metadata, error) in
guard metadata != nil else {
// エラーならインジケータを消す
indicator.dismiss()
return
}
// 次のアップロード...
let nextTask = nextFileReference.putData(nextData, metaData: nextMetaData) { (metadata, error) in
guard metadata != nil else {
// エラーならインジケータを消す
indicator.dismiss()
return
}
// 更にその次の...
let furtherTask = ...
}
}
監視の場合。
let uploadTask = createFirebaseStorageUploadTask()
// 処理結果の監視
uploadTask.observe(.success) { _ in
// 次のアップロード処理
let nextUploadTask = createNextFirebaseStorageUploadTask()
nextUploadTask.observe(.success) { _ in
let futherTask = createFurtherTask()
...
}
nextUploadTask.observe(.failure) { _ in
...
}
}
uploadTask.observe(.failure) { _ in
// エラーのとき
// Progress Indicatorを消す
indicator.dismiss()
}
これが複数のファイルアップロードになると、考慮することが増えてしまいます。 例えば、インジケータの表示非表示のタイミングだったり、各ステータスを監視して全部の処理が成功だったら……失敗だったらどこまで巻き戻すか……など。
DispatchSemaphoreを使った場合 #
DispatchSemaphoreを使うと、この処理が簡単になりました。
まずは基本構造 #
DispatchSemaphoreを定義し、待ち合わせが発生するタイミングでwait()
を呼ぶ。
処理が終了したら、signal()
を呼びます。
関数がネストしていますが、これは後ほど。。
func uploadTask() -> (() -> Result<Void, Error>) {
return {
// セマフォの初期化
let semaphore = DispatchSemaphore(value: 0)
var result: Result<Void, Error>!
let uploadTask = fileReference.putData(data, metadata: metaData) { (metadata, error) in
guard metadata != nil else {
result = .failure(UploadCustomError())
// 処理終了でセマフォをインクリメント
semaphore.signal()
return
}
result = .success(())
// 処理終了でセマフォをインクリメント
semaphore.signal()
}
// デクリメントして待つ
semaphore.wait()
// 後続処理
return result
}
}
こうすることにより、resultの変数に値が入るまで、非同期処理の結果を待つことができます。
タイムアウトの追加 #
タイムアウトの時間を設定しておくことで、待ちっぱなしを避けることができます。
// デクリメントして待つ(タイムアウト付き)
switch semaphore.wait(timeout: .now() + 30.0) {
case .success:
break
case .timedOut:
uploadTask.cancel()
result = .failure(TimedOutError())
StorageManager.shared.delete(fileName)
}
return result
別スレッドで実行、結果をメインスレッドに返す #
さらに、一連の処理をGlobal Dispatch Queueに突っ込むことで、UIスレッドを止めずに処理を行うことができます。
// on main thread
indicator.show()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
let firstTask = uploadTask()
let secondTask = secondUploadTask()
let result = firstTask().flatMap { secondTask() }
// UIスレッドに結果を返す
DistpatchQueue.main.async { [weak self] in
guard let self = self else { return }
switch result {
case .success:
// サクセス時の処理
case .failure(let e):
// 失敗時の処理
}
self.indicator.dismiss()
}
まとめ #
非同期処理を同期的に待つことのできるDispatchSemaphore
を使った実装について紹介しました。
説明がわかりにくい部分があったかと思います。私が実装した際は、以下の記事を参考にしたので、そちらも参照いただけると幸いです。おそらくこの記事で出てきた疑問点は解決できるのではないかなと思います。
(参考資料)