【Swift Concurrency】Task構造体の使い方!非同期タスク操作方法
この記事からわかること
- Swift Concurrencyとは?
- Task構造体の使い方
- 非同期処理の実装方法
- タスクの操作方法
- async letとの違い
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.1
- Swift:5.9
- macOS:Sonoma 14.1
公式リファレンス:Concurrency
公式ドキュメント:Concurrency
Swift Concurrencyとは?
Swift Concurrency(同時実効性)とはiOS15(Swift 5.5)から導入された仕組みの1つで非同期プログラミング(並列処理)をより利用しやすくするための機能を提供しています。async
やawait
キーワード、Task
構造体などはSwift Concurrencyから提供されており、非同期処理を実装する際に利用されるcompletionHandler(コールバック関数)の弱みである可読性の低下を解消することができるようになりました。
Task構造体とは?
Task
構造体は非同期関数(メソッド)を管理するための構造体です。ここでいう非同期関数とはasync
キーワードの付与された関数のことでawait
キーワードを使用して呼び出されます。そのまま呼び出そうとすると'async' call in a function that does not support concurrency
というエラーになってしまうので実行するにはTask
構造体のクロージャーとして渡すことで解決できます。
iOSではタスクが非同期作業の単位であり、非同期処理は1つのタスクの中で処理されます。Task.init
の定義にはクロージャーを受け取り、そのクロージャーにはasync
が付いています。
init(priority: TaskPriority?, operation: () async -> Success)
そのためTrailing Closure記法で省略してTask{}
と記述できます。
Task {
print("1")
let data = await fetchData() // 2秒かかる処理
print("2")
}
タスクインスタンスを作成後、クロージャーに渡した処理は即座に実行され、またこのインスタンスを使用してタスクの操作を行うことができるようになります。
またTask
はthrows
キーワードのついたエラーが発生する可能性のある関数もTask
内でdo-catch
文を記述しなくても実行することが可能です。しかしエラーが発生しても何も起きないのでエラーハンドリングが必要であればdo-catch
文を中に記述する必要があります。
Task {
let data = try await fetchData()
}
並列処理
タスクは非同期処理のため直列ではなく並列で処理されます。
func fetchData() -> Data {
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 1秒停止
print("非同期処理1")
}
Task {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2秒停止
print("非同期処理2")
}
Task {
try await Task.sleep(nanoseconds: 3 * 1_000_000_000) // 3秒停止
print("非同期処理3")
}
return Data()
}
print("START")
let data = fetchData()
print("END")
例えば上記のように関数の中に3つのタスクを記述して実行してみます。するとそれぞれの出力タイミングは以下のようになります。
START // 実行開始から直後
END // 実行開始から直後
非同期処理1 // 実行開始から1秒後
非同期処理2 // 実行開始から2秒後
非同期処理3 // 実行開始から3秒後
実行ボタンを押下してから3秒後には全て出力されており、例えば1つ目の1秒を待たずに2つ目が秒数のカウントを始めていることが確認できます。図に表すと以下のような感じでしょうか。
◇並列
◾️
◾️◾️
◾️◾️◾️
◇直列
◾️
◾️◾️
◾️◾️◾️
直列処理
複数の非同期処理を直列に実行したい場合は1つのタスクの中でawait処理を記述すればOKです。
func fetchData() -> Data {
Task {
try await Task.sleep(nanoseconds: 1 * 1_000_000_000) // 1秒停止
print("非同期処理1")
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2秒停止
print("非同期処理2")
try await Task.sleep(nanoseconds: 3 * 1_000_000_000) // 3秒停止
print("非同期処理3")
}
return Data()
}
print("START")
let data = fetchData()
print("END")
非同期処理をループで作成して直列に実行する
forループなどで処理を回して非同期処理を生成し、それを直列に実行したい場合は以下のように実装することで実現できます。
- 対象の非同期処理(printNumber)を定義
- 非同期処理を直列で実行するためのメソッド(runSerialTasks)を定義
- 直列で実行する処理を順番に保持する配列(tasks)を用意
- 処理を配列の中に詰めていく
- Taskの中で処理を順番に実行するメソッドを呼び出す
func printNumber(num: Int) async -> Bool {
try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2秒待機
print("\(num)")
return true
}
// 直列に非同期処理を実行する関数
func runSerialTasks(tasks: [() async -> Bool]) async {
for task in tasks {
let success = await task()
if success {
print("Task completed successfully")
} else {
print("Task failed")
}
}
print("All tasks completed")
}
let nums = Array(1...10)
var tasks = [() async -> Bool]()
for num in nums {
// ここでは配列に詰めるだけ
tasks.append {
return await printNumber(num: num)
}
}
Task {
// 作成したタスクを直列に実行開始
await runSerialTasks(tasks: tasks)
}
タスクの操作
Task
インスタンスを取得した後で様々な操作が可能になります。
処理を停止する
タスク内の処理を停止させるにはTask.sleep(nanoseconds:)
メソッドを使用します。これはstatic
なのでインスタンス化しなくても呼び出すことが可能です。sleepメソッドもasync throws
なのでtry await
が必要になります。
func fetchData() async throws -> Data {
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2秒停止
let data = Data() // 本来ならサーバーからデータ取得など(成功 or 失敗)
return data
}
引数にはナノ秒で渡す必要があるので2秒なら2 * 1_000_000_000(2,000,000,000ns)
になります。
タスクのキャンセル
タスクをキャンセルするにはcancel
メソッドを実行します。実行すると非同期関数が実行されなくなります。タスクがキャンセルされるとCancellationError
型のエラーをスローします。例えば以下のように実行すると1が出力された後にError: CancellationError()
と出力されます。
let myTask = Task {
do {
print("1")
let data = try await fetchData()
print("2")
} catch {
print("Error: \(error)") // Error: CancellationError()
}
}
myTask.cancel()
メインスレッドを避ける
Task
のクロージャーには@MainActor
が付与されているので常にメインスレッドで実行されます。しかしUIの更新を挟まないようなファイルへ保存するだけの処理などは、メインスレッドで実行するとUIのレスポンスが悪くなるだけなので避けたい場合のためにdetached
メソッドが用意されています。これでTask.detached
内の処理がバックグラウンドスレッドで実行されるようになります。
Task {
// メインスレッドで実行される
}
Task.detached {
// バックグラウンドスレッドで実行される
}
ですが調べてみるとasync
キーワードを付与した関数は基本的にバックグラウンドスレッドで実行されると記述されてありました。
参考文献:[swift] メインスレッドから処理を逃すために Task.detached を使う必要はない(ことが多い)
実際にスレッドを確認してみると確かにasync
内はバックグラウンドスレッド、Task
内はメインスレッドになっていました。
func fetchData() async throws -> Data {
print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
sleep(2)
let data = Data() // 本来ならサーバーからデータ取得(成功 or 失敗)
return data
}
Task {
print(Thread.current) // <_NSMainThread: 0x600001700140>{number = 1, name = main}
let data = try await fetchData()
print(Thread.current) // <_NSMainThread: 0x600001700140>{number = 1, name = main}
}
Task.detached
でバックグラウンドスレッドに変更される部分は非同期関数とその一連の処理として囲った部分になるようです。
Task.detached {
print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
let data = try await fetchData()
print(Thread.current) // <NSThread: 0x600001737700>{number = 6, name = (null)}
}
async letとの違い
並列処理を行う際にawait
を使用することでその処理が完了するまで一時停止させることができましたが、異なる非同期関数をさらに並列に処理させたい場合もあると思います。例えば異なる画像をダウンロードする3つの処理があるとしてawait
を使用すると1つ目の画像のダウンロードが完了してから2つ目のダウンロードが開始します。しかし実際は3つ同時にダウンロードして欲しいのでTask
で1つずつ囲っても良いですが、async let
を使用することですっきりと並列処理を記述できます。
Task {
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
}
async let
で変数を宣言することでその関数の処理を待たずに次の処理を実行します。取得した結果を参照したい箇所でawait
を使用して取得することが可能です。
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。