【Swift Concurrency】Task構造体の使い方!非同期タスク操作方法
この記事からわかること
- Swift Concurrencyとは?
- Task構造体の使い方
- 非同期処理の実装方法
- タスクの操作方法
- async letとの違い
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.3
- iOS:18.4
- Swift:6
- macOS:Sequoia 15.4
公式リファレンス: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)
Selfの参照について
Task.initの定義を確認すると@escapingが付与されていることを確認できます。@escapingは「関数が後から実行されることを示す属性」でした。またその場合は[weak self]でSelfを強参照しないようにすることが推奨されています。
public init(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async -> Success)
ただTask.initに関しては特殊で直後のguard let self else { return }ではなくawait直後にguard let self else { return }を入れ込まないと強参照になってしまう恐れがあるようです。
Task { [weak self] in
// ここでreturnしてもあまり意味がない
// guard let self else { return }
let data = try await fetchData()
// awaitの後にguard let selfをする
guard let self else { return }
// 安全にselfを参照
self.textView.text = data.text
}
詳しい仕組みは以下の記事が参考になるので見ておいてください。
公式リファレンス:`Task.init` に渡すクロージャが暗黙的に `self` をキャプチャすることの背景と注意点
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。





