概要
Swiftで並列処理を書けることを知らなかったため、記事として残します。
並行処理と並列処理
非同期処理における並行処理と並列処理について振り返ります。
食事の例で例えてみます。
健康的なのかそうじゃないのか絶妙なラインの食事を取ろうとするAさんは、
- 肉を温め直す(1分)
- カップラーメンの準備(3分)
- カット野菜を盛り付ける(2分)
の工程を行おうとしています。
この工程を、並行処理と並列処理でどう変化するのかを確かめてみます。
並行処理
ひとつひとつタスクが終わるのを待つ非効率な処理です。
ラーメンをふやかしているときは人間なら他のタスクができるので、勿体無いですよね。
全体で6分かかってしまっています。
並列処理
並列処理は、他のタスクを実行中に他のタスクを進める処理です。この中で一番時間のかかるラーメンの3分が出来上がることには、他のタスクが完了しているので、計3分で全てのタスクが完了します。
ですが、Aさんの体力(メモリ)が無さすぎた場合、タスクを処理しきれずエラーにつながります。
Swiftで実装
それでは、実際に実装し、処理速度の違いについて確かめてみます。
今回検証するAPIは、MusicKitAPIという、Appleが提供しているAPIです。
Apple Musicに搭載している曲はこちらのAPIでも取得可能で、アーティスト情報、楽曲情報を取得できるので超優秀です。
MusicKitの使用については、下記サイトを参考にしました。この場を借りて感謝申し上げます。
曲のIDから詳細情報を取得する
今回検証するシチュエーションは、曲IDから曲のタイトルや写真などの曲の詳細情報を取得する処理を複数回を行う処理です。まずは、曲のIDから詳細情報を取得するコードです。
曲のIDから詳細情報を取得
func fetchSong(songId: String) async -> Song? {
do {
let request = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: MusicItemID(songId))
let response = try await request.response()
return response.items.first
} catch {
print("[ERROR] 曲の取得に失敗しました。id: \(songId)")
return nil
}
}
シチュエーション画像
セットリストのロード中 | セットリストのロード完了 |
---|---|
![]() |
![]() |
並行処理で取得する
まずは、一つ一つ行う並行処理を行った場合のコードと処理速度を確かめます。
このとき、songIDsは、50個あります(Artistはミセスです)。
並行処理のいいところは、順番が保持される点ですね。
コード(並行処理)
func load(songIDs: [String]) async -> [Song] {
var songs: [Song] = []
for songID in songIDs {
let manager = MusicKitManager.shared
if let song = await manager.fetchSong(songId: songID) {
songs.append(song)
}
}
return songs
}
結果(並行処理)
検証回数 | 時間(秒) |
---|---|
1回目 | 10.02 |
2回目 | 9.73 |
3回目 | 22.85 |
平均 | 14.2 |
3回検証した結果、50曲取得にかける平均時間は14.2秒でした。ストレスですね。
並列処理で取得する
次に、同時に処理を行う並列処理を行った場合のコードと処理速度を確かめます。
取得する条件は、並行処理と同様で50曲です。
曲順を担保するために、辞書型を用いています。
※ただし、一度に50曲分のリクエストを行うと429エラー(リクエストしすぎ)が出てくるので、意図的に0.2秒遅延させています。
コード(並列処理)
func load(songIDs: [String]) async -> [Song] {
var indexedSongs = [Song]()
indexedSongs = await withTaskGroup(of: (Int, Song?).self) { group in
for (index, songID) in songIDs.enumerated() {
group.addTask {
//429エラー防止
try? await Task.sleep(nanoseconds: 200_000_000)
let song = await MusicKitManager.shared.fetchSong(songId: songID)
return (index, song)
}
}
var songDict: [Int: Song] = [:]
for await (index, song) in group {
if let song = song {
songDict[index] = song
}
}
// index順に戻す
return songIDs.indices.compactMap { songDict[$0] }
}
return indexedSongs
}
結果(並列処理)
検証回数 | 時間(秒) |
---|---|
1回目 | 2.03 |
2回目 | 0.94 |
3回目 | 1.08 |
平均 | 1.35 |
3回検証した結果、50曲取得にかける平均時間は1.35秒でした。高速です。
まとめ
並行処理と並列処理を比較してみると、並列処理の方が12.85秒速いという結果になりました。
とても便利ですね!ただし、並列処理は、メモリ使用量が増える点や、大量のリクエストを一度に並列実行すると、サーバー側のレート制限に引っかかる(429 Too Many Requests)可能性があるので、全ての一度にという訳に行きません。今回のように、意図的に遅延を設けるなどの工夫が必要かもしれません。
今回の検証したアプリはこちら↓