• トップ
  • ブログ一覧
  • 【iOS】Swift Concurrencyに入門してみた
  • 【iOS】Swift Concurrencyに入門してみた

    いまむー(エンジニア)いまむー(エンジニア)
    2024.01.23

    IT技術

    はじめに

    Swift Concurrencyを勉強したので、備忘録を残しておこうと思います

    Swift Concurrency

    Swift Concurrencyとは、Swift5.5から導入された言語機能で非同期処理と並列処理の実装をサポートします

    async/await

    関数のreturn矢印の前にasyncをつけることで非同期関数として扱えるようになり、
    awaitをつけて呼び出すことで処理が終わるまで実行が一時停止します

    以下はコールバック処理で書いた場合とそれをasync/awaitに書き換えた場合の例です

    1// コールバックで書いたもの
    2func hello(completion: (String) -> ()) {
    3    sleep(3) // 時間のかかる処理
    4    completion("Hello")
    5}
    6
    7func world(completion: (String) -> ()) {
    8    sleep(3) // 時間のかかる処理
    9    completion("World")
    10}
    11
    12// 呼び出し
    13hello { hello in
    14    world { world in
    15       print(hello + world)
    16    }
    17}
    1// async/awaitで書いたもの
    2func hello() async -> String {
    3    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    4    return "Hello"
    5}
    6
    7func world() async -> String {
    8    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    9    return "World"
    10}
    11
    12// 呼び出し
    13Task {
    14    let hello = await hello()
    15    let world = await world()
    16    print(hello + world)
    17}

    比較してみると、コールバックではネストする必要がありますが、async/awaitでは同期的にコードを書けていることが分かります

    エラーが発生する非同期関数では、throwsの前にasyncをつけることで非同期関数にします
    呼び出し時には、通常と同じようにtryをつけて呼び出します

    1// エラーが発生する場合
    2func hello() async throws -> String {
    3    try await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    4    return "Hello"
    5}
    6
    7func world() async throws -> String {
    8    try await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    9    return "World"
    10}
    11
    12// 呼び出し
    13Task {
    14    do {
    15        let hello = try await hello()
    16        let world = try await world()
    17        print(hello + world)
    18    } catch {
    19        print("error")
    20    }
    21}

    async let

    async letを使うことで非同期関数を並列に実行することができます

    1func hello() async -> String {
    2    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    3    return "Hello"
    4}
    5
    6func world() async -> String {
    7    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    8    return "World"
    9}
    10
    11// 呼び出し
    12Task { 
    13    async let hello = hello()
    14    async let world = world()
    15    await print(hello + world)
    16}

    コールバック関数を非同期関数へ変換

    withCheckedContinuationを使うことで既存のコールバック関数を非同期関数へ変換することができます
    エラーが発生するコールバック関数の場合は、withCheckedThrowingContinuationを使用します

    1// コールバック関数を非同期関数に変換
    2// コールバック関数
    3func hello(completion: (String) -> ()) {
    4    sleep(3) // 時間のかかる処理
    5    completion("Hello")
    6}
    7
    8// 非同期関数に変換
    9func asyncHello() async -> String {
    10    return await withCheckedContinuation { continuation in
    11        hello { hello in
    12            continuation.resume(returning: hello)
    13        }
    14    }
    15}
    16
    17// 呼び出し
    18Task {
    19    let hello = await asyncHello()
    20    print(hello)
    21}
    1// エラーが発生するコールバック関数を非同期関数に変換
    2struct MyError: Error {}
    3// エラーが発生するコールバック関数
    4func hello(completion: (Result<String, Error>) -> ()) {
    5    sleep(3) // 時間のかかる処理
    6    let hasError = Bool.random()
    7    if hasError {
    8        completion(.failure(MyError()))
    9    } else {
    10        completion(.success("Hello"))
    11    }
    12}
    13
    14// 非同期関数に変換
    15func asyncHello() async throws -> String {
    16    return try await withCheckedThrowingContinuation { continuation in
    17        hello { result in
    18            continuation.resume(with: result)
    19        }
    20    }
    21}
    22
    23// 呼び出し
    24Task {
    25    do {
    26        let hello = try await asyncHello()
    27        print(hello)
    28    } catch {
    29        print("Error")
    30    }
    31}

    Task

    タスクは非同期処理の実行単位です
    Taskを使うことで同期関数から非同期関数を呼び出すことができるようになります

    Task.initは現在の実行環境を引き継ぎます
    @MainActorでTask.initを使った場合、タスクは@MainActorで実行されます

    1// 呼び出し
    2Task {
    3    let hello = await hello()
    4    print(hello)
    5}
    6
    7// @MainActorとすることで明示的にMainActorで実行
    8Task { @MainActor in
    9    let hello = await hello()
    10    print(hello)
    11}

    Task.detachedは現在の実行環境を引き継ぎません
    メインスレッドで実行する必要がないものはTask.detachedを使用します

    1// 呼び出し
    2Task.detached {
    3    let hello = await hello()
    4    print(hello)
    5}

    Taskのインスタンスを保持することでタスクをキャンセルすることができます
    以下のコードでは、タスクをキャンセルすることでworld()が実行されずにエラーが発生して終了します

    1func hello() async throws -> String {
    2    try Task.checkCancellation()
    3    sleep(3)
    4    return "Hello"
    5}
    6
    7func world() async throws -> String {
    8    try Task.checkCancellation()
    9    sleep(3)
    10    return "World"
    11}
    12
    13// 呼び出し
    14let task = Task {
    15    do {
    16        let hello = try await hello()
    17        let world = try await world()
    18        print(hello + world)
    19    } catch {
    20        print("error")
    21    }
    22}
    23
    24// タスクをキャンセル
    25task.cancel()

    TaskGroup

    TaskGroupを使うことで並列処理を実装することができます
    TaskGroupの作成にはwithTaskGroupを使用します
    エラーが発生する場合はwithThrowingTaskGroupを使用します

    以下のコードではhello()とworld()が並列に実行され、"HelloWorld"または"WorldHello"で出力されます

    1func hello() async -> String {
    2    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    3    return "Hello"
    4}
    5
    6func world() async -> String {
    7    try? await Task.sleep(until: .now + .seconds(3)) // 時間のかかる処理
    8    return "World"
    9}
    10
    11Task {
    12    var printText = ""
    13    await withTaskGroup(of: String.self) { group in
    14        // タスクを作成
    15        group.addTask {
    16            return await hello()
    17        }
    18
    19        // タスクを作成
    20        group.addTask {
    21            return await world()
    22        }
    23
    24        for await text in group {
    25            printText += text
    26        }
    27    }
    28    print(printText)
    29}

    タスクのキャンセル

    CancellationErrorをスローすることでタスクをキャンセルできます
    タスクがキャンセルされている場合、Task.checkCancellationではCancellationErrorがスローされます
    また、Task.isCancelledでタスクがキャンセルされているか判定することもできます

    1func hello() async throws -> String {
    2    // タスクがキャンセルされている場合、エラーをスローして処理を抜ける
    3    try Task.checkCancellation()
    4    sleep(3)
    5    return "Hello"
    6}
    7
    8func world() async throws -> String {
    9    // タスクがキャンセルがされているか判定
    10    if Task.isCancelled {
    11        // エラーをスローして処理を抜ける
    12        throw CancellationError()
    13    }
    14    sleep(3)
    15    return "World"
    16}
    17
    18// 呼び出し
    19let task = Task {
    20    do {
    21        let hello = try await hello()
    22        let world = try await world()
    23        print(hello + world)
    24    } catch {
    25        print("error")
    26    }
    27}
    28
    29// タスクをキャンセル
    30task.cancel()

    actor

    actorはデータ競合を防ぐ型でclassと同じ参照型です
    プロパティ、メソッド、イニシャライザの定義とプロトコル準拠ができますが、actorの型を継承することはできません
    プロパティやメソッドを使う時にawaitをつけることでデータ競合を防ぐ仕組みがあります
    actorのプロパティは外から直接更新することはできません
    nonisolatedをつけることでactor管理から外れ、awaitなしでアクセスできるようになります

    1// actorを使っていない場合
    2var count = 0
    3
    4func increment() async -> Void {
    5    count += 1
    6}
    7
    8// 呼び出し
    9// 複数同時に呼び出しすると、1から順番に出力されない場合がある
    10Task.detached {
    11    await increment()
    12    print(count)
    13}
    1// actorを使う場合
    2actor Counter {
    3    var count = 0
    4
    5    func increment() {
    6        count += 1
    7    }
    8}
    9
    10let counter = Counter()
    11
    12// 呼び出し
    13// 複数同時に呼び出しても、1から順番に出力される
    14Task {
    15    await counter.increment()
    16    print(await counter.count)
    17}

    @MainActor

    @MainActorに適応したものはメインスレッドで実行されます
    nonisolatedをつけることでMainActor管理から外れ、awaitなしでアクセスできるようになります
    型、コンピューテッドプロパティ、メソッドに@MainActor属性を付けることができますが、ストアドプロパティーに@MainActorを適応することはできません

    1// 型に@MainActorを適応
    2@MainActor
    3struct Hoge {}
    4
    5struct Fuga {
    6    private var _hoge = "hoge"
    7
    8    // コンピューテッドプロパティに@MainActorを適応
    9    @MainActor
    10    var hoge: String {
    11        get { _hoge }
    12        set { _hoge = newValue }
    13    }
    14
    15    // メソッドに@MainActorを適応
    16    @MainActor
    17    func fuga() {}
    18}

    Async Sequence

    Async Sequenceは要素への非同期、順次、反復アクセスを提供する型で、filter、contains、mapなどのメソッドが標準で用意されています
    AsyncSequenceとAsyncIteratorProtocolに準拠することで独自の型を作成することができます

    1struct AsyncTimer: AsyncSequence {
    2    typealias Element = Int
    3
    4    struct AsyncIterator: AsyncIteratorProtocol {
    5        private var count = 0
    6        private var date = Date()
    7
    8        mutating func next() async -> Element? {
    9            guard count < 10 else {
    10                // nilを返すとループが終了
    11                return nil
    12            }
    13
    14            while date.distance(to: Date()) < 1 {
    15                try? await Task.sleep(nanoseconds: 1 * 10_000_000)
    16            }
    17
    18            date.addTimeInterval(1)
    19            count += 1
    20            return count
    21        }
    22    }
    23
    24    func makeAsyncIterator() -> AsyncIterator {
    25        return AsyncIterator()
    26    }
    27}
    28
    29// 呼び出し
    30Task {
    31    let double = AsyncTimer().map { $0 * 2 }
    32
    33    for await time in double {
    34        print(time)
    35    }
    36}

    Sendable

    Sendableはデータ競合を防ぐための型です
    actorは参照型ですが、同時アクセスを防ぐためSendableといえます
    外からactor内のメソッドを呼び出す場合には、引数はSendableな値である必要があります
    @Sendableをメソッド、クロージャにつけることで、Sendableとして扱うことができます
    structとenumはSendableな値(プロパティ、メソッドなど)のみを持つ場合、暗黙的にSendableに準拠されます(publicな型の場合は例外)

    1struct Hoge: Sendable {}
    2
    3@Sendable
    4func hoge() {}
    5
    6let fuga: (@Sendable () -> Void) = {}

    おわりに

    今回、Swift Concurrencyの書き方をまとめてみました。
    Swift Concurrencyを使用する時には、この記事を見返しながら実装をしたいと思います。

    この記事が何かお役に立てば幸いです。
    ここまでご覧いただきありがとうございました。

    いまむー(エンジニア)

    いまむー(エンジニア)

    おすすめ記事