15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Swift]Swift Concurrencyを学ぶ過程を記録してみた

Posted at

始めに

"何となくわかる"からの脱却を目指す。
記事を見ていき、理解があいまいな箇所は深掘りしていく。

間違いなどあれば、コメントにて教えて欲しいです!🙇

参考1

Taskとは?

公式には以下のように記されていた。

A unit of asynchronous work.

When you create an instance of Task, you provide a closure that contains the work for that task to perform.Tasks can start running immediately after creation; you don’t explicitly start or schedule them. After creating a task, you use the instance to interact with it — for example, to wait for it to complete or to cancel it.

A task’s execution can be seen as a series of periods where the task ran. Each such period ends at a suspension point or the completion of the task.

Taskは非同期処理の実行単位!

別の記事では以下のように記されていた。

セッションでは、タスクの持つ基本的な性質について紹介されています。まず、タスクは非同期的な処理を並行に(concurrently)コードを実行するためのコンテキストを提供するものです。スレッドと違い、複数のタスクを "安全で効率的なときだけ" 並列で(parallely)実行します。タスクは Swift の言語機能と統合されているので、Swift コンパイラは並行処理におけるバグを事前に発見して警告することができます。そして、タスクは async のついた関数を呼び出すたびに作られるわけではなく、ある非同期な関数から他の非同期な関数を呼び出したときはそのまま同じタスクで実行されます。タスクを作る方法・作られるタイミングは決まっていて、必ず明示的に作られるものです。

こういったタスクという概念やタスク間に作られる依存関係は単なる Swift 内部の実装詳細というわけではなく、後述するキャンセルや優先度などを理解する上で重要な概念である、とも説明されています。

以下の記事は、Taskの並行処理について実際にコードで実装し、キャプチャで残してくれている!直列処理や全てのタスク実行を待つやり方などが記載されているので参考にする!

Taskは以下のように、非同期処理のasyncメソッドを囲むように使用する。
これは前述したように、Taskは非同期処理の実行単位なので、asyncメソッドの実行が可能になるから!

Task {
    do {
        try await fetchHogeAPI()
    } catch {
        print(error)
    }
    print("call2")
}

なので、以下のように実行すると、同期メソッドからasyncメソッドを呼び出すことになるのでエラーになる。

do {
    try await fetchHogeAPI()
} catch {
    print(error)
}

公式には以下のように記されていた。

A task is a unit of work that can be run asynchronously as part of your program. All asynchronous code runs as part of some task.

すべての非同期コードは、何らかのタスクの一部として実行される。

でも、Taskが非同期処理の実行単位とは何だろう?
公式に記載されている初期化処理を見てみる。

// ①
@discardableResult
init(
    // ②
    priority: TaskPriority? = nil,
    // ③
    operation: @escaping () async -> Success
)

@discardableResult
init(
    priority: TaskPriority? = nil,
    // ④
    operation: @escaping () async throws -> Success
)
①について

@discardableResultをつけることによって、返り値を気にする必要がなくなる。(参考)
例えば、以下のような感じ!Sample1のinit()に対してつけた場合と、つけなかった場合では警告表示有無が異なる!

// つけない場合
class Sample1 {
    init<Success>(priority: TaskPriority? = nil, operation: @escaping () async -> Success) {
        // インスタンスの初期化処理
    } 
}

// 呼び出し側に警告が出る
Sample1(operation: {
// 警告: Rsult of 'Sample1' initializer is unused
// 非同期処理
})

// つけた場合
class Sample1 {
    @discardableResult
    init<Success>(priority: TaskPriority? = nil, operation: @escaping () async -> Success) {
        // インスタンスの初期化処理
    } 
}

// 呼び出し側に警告が出ない
Sample1(operation: {
// 非同期処理
})

②について

priorityで優先度を指定することができ、初期値がnilで入っている。
優先度についての参考

③について

operationはクロージャであるが、中身を見てみると() async -> Successと記載がある。つまり、パラメーターなしの非同期処理で返り値がSuccessの関数がoperationに入るということ!そのためクロージャの中身に非同期処理を書くことが許されている!そして返り値のSuccessについては、ジェネリクスであって、あらゆる型を指定できるということも大事!(参考)

④について

エラーがスローされる場合は、Successを返すかエラーをスローする!

省略せずに書いてみる。
.valueによって、Successを取得できる。

// エラーなしで値が返ってくる場合
func fetchHogeAPI() async -> String {
    return "Hoge"
}
// ①
let result = await Task.init(operation: {
    return await fetchHogeAPI()
}).value

// エラーありで値が返ってくる場合
func fetchHogeAPI() async throws -> String {
    return Error
}
// ②
do {
    let result = try await Task.init(operation: {
        try await fetchHogeAPI()
    }).value
} catch(let error) {
    // エラー処理
}
①について

.valueは、公式に以下のように記載がある。

The result from a nonthrowing task, after it completes.

var value: Success { get async }

.valueSuccessを返してくれる。

②について

エラーをスローする場合の.valueについては、公式に以下のような記載がある。

The result from a throwing task, after it completes.
If the task throws an error, this property propagates that error.

var value: Success { get async throws }

伝播されたエラーはどこかでキャッチしてあげないといけない!

でも以下のような書き方の方がよくするけど、これはSuccessの返り値はどうなるのか?

Task {
    do {
        try await fetchHogeAPI()
    } catch {
        print(error)
    }
    print("call2")
}

Successはジェネリクスなので型は決まっていない。この場合、返り値の型はVoidになるため、返り値を返さなくてもエラーになることはない!

実行順序が頭の中で整理できなくなる時があるので、下図のイメージを頭に入れて使いたい‥。("Taskとは何なのか"から参照)
スクリーンショット 2023-06-16 10.55.05.jpg

Task.detachedとは?

Task.initの公式ページに以下のように記載があった。

Unlike Task.detached(priority:operation:), the task created by Task.init(priority:operation:) inherits the priority and actor context of the caller, so the operation is treated more like an asynchronous extension to the synchronous operation.

Task.initによって作成されたタスクは呼び出し元の優先度とアクターコンテキストを継承する。逆にTask.detachedは継承しない。
例えば、メインスレッドで実行される処理をバックグラウンドで実行したい場合や、バックグラウンドで実行される処理の一部をメインスレッドで実行したい場合など。

Task.initとTask.detachedを使うケースについては以下がわかりやすかった。

参考2

GlobalActorとは?

GlobalActorについて、公式には以下のように記載があった。

However, when the data that needs to be isolated is scattered across a program, or is representing some bit of state that exists outside of the program, bringing all of that code and data into a single actor instance might be impractical (say, in a large program) or even impossible (when interacting with a system where those assumptions are pervasive).

A primary motivator of global actors is to apply the actor model to the state and operations that can only be accessed by the main thread.

Global actors provide the mechanism for describing the main thread in terms of actors, utilizing Swift's actor isolation model to aid in correct usage of the main thread.

上記内容を説明してくれている記事。わかりやすい!

ChatGPTにもGrobalActorとMainActorの関係性について聞いてみた。
わかりやすかったので記録!

GlobalActorとMainActorは、Swift 5.5以降で導入された並行処理のための2つの関連した機能です。

GlobalActorは、グローバルなスコープにアクターを導入するためのアトリビュートであり、特定のグローバルアクターに属する関数やプロパティ、型などを指定します。これにより、それらの要素はグローバルアクターのコンテキスト内でのみ操作が許可されるようになります。

一方、MainActorは特別なグローバルアクターであり、アプリケーションのメインスレッド上で実行されるコンテキストを表します。MainActorは、ユーザーインターフェースの更新などのUI関連の操作を安全に行うために使用されます。

MainActorには特別な振る舞いがあり、MainActorに属する関数やプロパティは自動的にメインスレッドで実行されるようになります。これにより、メインスレッドでのUIの更新やイベントの処理などが簡単かつ安全に行えます。

GlobalActorとMainActorは、並行処理のためのコンテキストを提供するという点で類似していますが、MainActorは特にメインスレッド上での処理を行うためのものです。アプリケーションのメインスレッドは通常、ユーザーインターフェースの操作やUIの更新に関連しており、そのためMainActorが必要とされます。

GlobalActorとMainActorは、Swiftの並行処理モデルの一部として協調して動作し、スレッドセーフなコードの記述と実行の管理を支援します。アクターモデルという考え方に基づいており、アクターごとに明確な処理順序が保証され、競合状態やデッドロックのリスクを減らすことができます。

GlobalActorについて何となくわかったけど、いつ使うのか?
以下の記事をみるとイメージしやすかった!

独自のグローバルアクターを作成して、関連する全てのメソッドにアノテーションをつけて、グローバルアクター上で実行すれば、異なるスレッドが同時にデータを書き込むことによる同時実効性の問題を回避できる!
スクリーンショット 2023-06-17 11.24.12.jpg

使い方がわかったところで、以下の記事をみるとGlobalActorの作成の仕方も載っていて理解しやすい!

MainActorとは?

GlobalActorの一種で、特定の関数がメインスレッドでのみ実行されることを要求するために使用される。以下は公式に載っていたサンプル。

@MainActor var globalTextSize: Int

@MainActor func increaseTextSize() { 
  globalTextSize += 2   // okay: 
}

func notOnTheMainActor() async {
  globalTextSize = 12  // error: globalTextSize is isolated to MainActor
  increaseTextSize()   // error: increaseTextSize is isolated to MainActor, cannot call synchronously
  await increaseTextSize() // okay: asynchronous call hops over to the main thread and executes there
}

callback = { @MainActor in
  print($0)
}

callback = { @MainActor (i) in 
  print(i)
}

Actorはデータの競合を防ぐものであり、他から書き込みはできないのと、メソッドを実行する際は、awaitで停止することが必要。

UIViewControllerはデフォルトで@MainActorがついている!以下を参考にする。

Sendableとは?

値を並行処理で安全に扱うことができなければ、競合が起きる。Sendableは、その問題に対して、自分が"並行処理で安全に扱える値"であることを明示するためのもの!@Sendable属性がついた関数やクロージャは、クロージャ内で使うためにキャプチャする値が全てSendableプロトコルに準拠する必要があることを示す。

下記のコードの例はイメージがつきやすかった。
コードはMutation of captured var 'thumbnails' in concurrently-executing code("キャプチャされた 'thumbnails' 変数を並行に実行されるコードの中で変更しています")というコンパイルエラーになる。その理由としては、addTaskのクロージャが@Sendable属性をつけているから、キャプチャする全ての値は安全に扱える値でなければならないこと!並行処理の中で安全な値を使うためのSendableは、重要なもの!

// "Explore structured concurrency in Swift" 13:58
func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: Void.self) { group in
        for id in ids {
            group.addTask {
                thumbnails[id] = try await fetchOneThumbnail(withID: id)
            }
        }
    }
    return thumbnails
}

詳細は下記の記事をみる!とてもわかりやすい!

下記の記事もsendableの使い方と、actor内外からの処理についてとてもわかりやすい!

終わりに

まだまだ学ぶべきことはたくさんあるので、都度学べたことは追記していきます!
ここまでご覧いただきありがとうございました!

15
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?