Swiftのactorを完全に理解するための12のステップ – Japanシーモア

Swiftのactorを完全に理解するための12のステップ

Swiftのactorのイラストと12のステップで学ぶテキストSwift
この記事は約24分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

このサービスは複数のSSPによる協力の下、運営されています。

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Swiftの新しい機能である「actor」は多くの開発者の関心を引いています。

同時実行の問題を解決するこの機能は、プログラムのスレッドセーフを保ちながらも、コードの可読性やメンテナンス性を犠牲にすることなく、高速な処理を実現する手助けをしてくれます。

本記事では、Swiftの「actor」に焦点を当て、その基本から詳細、そして応用例までを徹底的に解説していきます。

サンプルコードとその解説を交えて、初心者から上級者まで、actorを使ったプログラミングの魅力を深く理解することができるように構築しました。

そして、12のステップで「actor」の全てを学ぶことができます。

●Swiftとactorとは

Swiftは、Appleが開発した高速で安全性の高いプログラム言語として知られています。

そんなSwiftには、多くの特性や新機能が存在しており、その中でも「actor」は非常に注目されています。

○Swiftの基本的な特性

SwiftはObjective-Cに代わる、iOSやmacOSのアプリケーション開発の主力として用いられています。

Swiftの特長としては、型の安全性や高速な実行、現代的な文法、そしてメモリ管理の仕組みなどが挙げられます。

近年の技術トレンドとして、マルチコアプロセッサの普及に伴い、同時実行や並行処理の需要が高まっています。

そのため、これに対応する新しい手法としてSwiftの「actor」が導入されました。

○actorの登場背景

多くのデバイスでマルチコアプロセッサが主流となる中、プログラムの同時実行や並行処理は一般的になってきました。

データの競合やスレッドの安全性といった問題も増えてきました。従来の手法には限界がありました。

Swiftの開発チームはこの問題に対処するため、「actor」という新しい同時実行の手法を導入しました。

これにより、データの競合やスレッドの安全性問題が大きく改善されることとなりました。

●actorの基本

Swift言語の進化の中で、最も注目されているのが「actor」です。

ここでは、actorの基本的な特徴や、それがもたらすメリット、そしてactorの基本的な宣言方法について詳しく解説します。

○actorの特徴とメリット

Swiftにおける「actor」は、同時実行の問題に対する強力な解決策として導入されました。

その主な特徴は次の通りです。

  1. スレッドセーフ:actor内のデータには、同時に複数のスレッドからのアクセスが許されません。これにより、データの競合や不整合を防ぐことができます。
  2. データの隔離:各actorは独自のデータを持ち、他のactorや外部からの直接のデータアクセスを許可しない構造になっています。
  3. 非同期のサポート:actorは非同期操作を自然にサポートしており、Swiftの新しいasync/await文法とも連携が取れます。

これらの特徴により、actorを利用することで次のメリットが得られます。

  • コードの可読性の向上:非同期処理や並行処理を、より直感的に書くことができます。
  • バグの減少:データの競合やスレッドの問題を自動的に避けるため、関連するバグのリスクが減少します。

●actorの詳細な使い方

actorはSwiftにおいて同時実行をより安全に行うための新しい概念です。

その詳細な使い方を学び、効果的に利用するための手法をここで解説します。

○サンプルコード1:基本的なactorの作成

まずは、基本的なactorの作成方法を学びましょう。

actor SimpleActor {
    var value: Int = 0
}

このコードではSimpleActorという名前のactorを定義しています。

この例ではactor内にvalueというInt型の変数を持っています。

actorの中には、変数や関数などを定義することができ、それらのアクセスがスレッドセーフになるように設計されています。

このコードを実際に利用する場面を考えると、例えばアプリケーション内でデータの更新を行う際に、複数のスレッドから同時にアクセスされることを防ぐために使用されます。

○サンプルコード2:actor内でのメソッドの呼び出し

actor内でメソッドを定義し、そのメソッドを外部から呼び出す方法を見ていきましょう。

actor SimpleActor {
    var value: Int = 0

    func increment() {
        value += 1
    }
}

let myActor = SimpleActor()
Task {
    await myActor.increment()
}

このコードではSimpleActor内にincrementというメソッドを定義しています。

この例ではメソッド内でvalueの値を1増やしています。actorのメソッドは外部から直接呼び出すことができません。

そのため、Taskという非同期のタスクを用いて、awaitを使ってメソッドを呼び出しています。

このコードを利用することで、複数のスレッドから同時にメソッドを呼び出しても、actor内部のデータの状態が一貫して保持されるため、データの不整合や競合を避けることができます。

○サンプルコード3:actor間でのデータのやりとり

Swiftの新機能であるactorを用いて、データのやり取りをする方法を解説していきます。

actorはスレッドセーフなデータアクセスを実現するための仕組みとして導入されましたが、異なるactor間でのデータのやり取りも重要なテーマの一つとなります。

actor MyActor {
    private var data: Int = 0

    func increment() {
        data += 1
    }

    func fetchData() -> Int {
        return data
    }
}

actor AnotherActor {
    private let myActor: MyActor = MyActor()

    func fetchDataFromMyActor() async -> Int {
        await myActor.fetchData()
    }
}

let anotherActor = AnotherActor()
Task {
    let fetchedData = await anotherActor.fetchDataFromMyActor()
    print(fetchedData)
}

このコードでは、MyActorという名前のactorが定義されており、その中にdataというプロパティと、それに関連するメソッドが定義されています。

次に、AnotherActorという別のactorが定義されており、この中でMyActorのインスタンスを持っています。

AnotherActor内のfetchDataFromMyActorメソッドでは、MyActorfetchDataメソッドを非同期で呼び出して、その結果を返しています。

最後に、AnotherActorのインスタンスを作成し、非同期タスク内でfetchDataFromMyActorメソッドを呼び出し、結果をコンソールに出力しています。

この例を実行すると、コンソールに0と表示されます。

これは、MyActordataが初期値として0を持っているためです。

○サンプルコード4:actorと非同期関数

Swiftのactorは非同期関数との連携も非常に重要です。

actor内のデータを安全に取得するため、非同期関数の利用が推奨されます。

actor SampleActor {
    private var items: [String] = []

    func addItem(_ item: String) {
        items.append(item)
    }

    func fetchItems() async -> [String] {
        await Task.sleep(1_000_000) // 1秒待機
        return items
    }
}

let actorInstance = SampleActor()
Task {
    await actorInstance.addItem("Apple")
    let resultItems = await actorInstance.fetchItems()
    for item in resultItems {
        print(item)
    }
}

このコードでは、SampleActorというactorを定義しています。

その中に、文字列のリストitemsと、そのリストにアイテムを追加するaddItemメソッド、アイテムを非同期で取得するfetchItemsメソッドがあります。

非同期関数fetchItems内では、1秒の待機処理を行った後に、itemsの内容を返しています。

最後の部分で、非同期タスクを用いて、アイテムを追加し、その後にアイテムのリストを取得してコンソールに出力しています。

この例を実行すると、コンソールにAppleと表示されます。

●actorの応用例

Swiftのactorは、非同期のタスクをより安全に実行するための新しい型です。

これにより、データの競合やスレッドの安全性問題を減少させることができます。

ここでは、actorを利用した実用的な応用例を2つ取り上げ、その具体的なコードとその動作について詳しく説明します。

○サンプルコード5:actorを用いたシンプルなカウンター

このコードでは、actorを使ってシンプルなカウンターの実装を行っています。

カウンターはインクリメントとデクリメントの操作が可能で、複数のスレッドから同時にアクセスされる場面でも正確に動作します。

// シンプルなカウンターのactor宣言
actor Counter {
    private var value: Int = 0

    // カウンターの値をインクリメント
    func increment() {
        value += 1
    }

    // カウンターの値をデクリメント
    func decrement() {
        value -= 1
    }

    // カウンターの現在の値を取得
    func currentValue() -> Int {
        return value
    }
}

この例では、Counterという名前のactorを定義しています。

valueはカウンターの内部の値を保持するプロパティで、privateになっているため外部からはアクセスできません。

incrementdecrementcurrentValueはそれぞれカウンターの値を増減させるメソッドや現在の値を取得するメソッドです。

このactorを使うと、例えば次のようなコードで複数のタスクから安全にカウンターを操作できます。

let counter = Counter()

Task {
    for _ in 1...1000 {
        await counter.increment()
    }
}

Task {
    for _ in 1...1000 {
        await counter.decrement()
    }
}

ここでは、2つの非同期のタスクがそれぞれ1000回ずつカウンターをインクリメントとデクリメントします。

actorの性質により、これらの操作は競合せずに順序良く実行されます。

実際に上記のコードを実行すると、最終的にカウンターの値は0になります。

これは、1000回のインクリメントと1000回のデクリメントが互いに打ち消し合うためです。

○サンプルコード6:actorを活用したデータベースの同時アクセス制御

データベースへの同時アクセスは、競合やデータの不整合を引き起こす可能性があるため注意が必要です。

actorを用いることで、このような問題を効果的に回避することができます。

このコードでは、データベースへの読み書きを行うシンプルなactorを実装します。

データベースとしては、ここでは簡単のために辞書型を用いて模擬的に表現します。

actor Database {
    private var data: [String: String] = [:]

    // データを追加または更新
    func set(key: String, value: String) {
        data[key] = value
    }

    // キーに対応するデータを取得
    func get(key: String) -> String? {
        return data[key]
    }
}

このactorでは、内部にdataという辞書型のプロパティを持っており、これを用いてデータベースの読み書きを行っています。

setメソッドではデータの追加や更新、getメソッドではデータの取得を行います。

actorを用いることで、複数の場所からこのDatabaseへのアクセスが行われた場合でも、それぞれの操作が順番に実行されるため、データの競合や不整合を回避することができます。

例えば、次のようなコードで複数のタスクからデータベースへのアクセスを行うことができます。

let database = Database()

Task {
    await database.set(key: "username", value: "Alice")
    print(await database.get(key: "username"))
}

Task {
    await database.set(key: "username", value: "Bob")
    print(await database.get(key: "username"))
}

このコードを実行すると、AliceBobが順番に出力されます。

これは、actorを通してデータベースへのアクセスが行われるため、互いの操作が競合せずに正しく動作することを意味しています。

○サンプルコード7:actorとCombineを組み合わせた例

SwiftのactorとCombineフレームワークを組み合わせることで、非同期処理をより効率的かつ安全に行うことが可能になります。

このコードではactorを使って非同期にデータを取り扱い、Combineを使ってそのデータをストリームとして扱い、結果を返すコードを表しています。

この例では、非同期にデータを取得してCombineのPublisherを用いてデータの更新を検知しています。

import Combine
import SwiftUI

// actorの宣言
actor DataProvider {
    private var data: [String] = []

    func addData(item: String) async {
        data.append(item)
    }

    func fetchData() async -> [String] {
        return data
    }
}

class DataManager: ObservableObject {
    private let provider = DataProvider()
    private var cancellables: Set<AnyCancellable> = []

    // Combineでのデータの監視
    @Published var items: [String] = []

    init() {
        // 非同期メソッドの結果をCombineのPublisherにバインド
        Task {
            let fetchedItems = await provider.fetchData()
            items = fetchedItems
        }
    }

    func addItem(item: String) {
        Task {
            await provider.addData(item: item)
            let updatedItems = await provider.fetchData()
            items = updatedItems
        }
    }
}

このコードでは、非同期に動作するDataProviderという名前のactorを用意しています。

このactorは、非同期にデータを追加したり取得したりする機能を持っています。

また、DataManagerというクラスはObservableObjectを採用しており、Combineを使用して非同期に取得したデータの変更を検知しています。

このコードを実行すると、DataProvider actorを通じてデータを非同期に追加または取得することができます。

さらに、そのデータの変更はCombineの@Published属性を使用して監視され、UIなどにデータの変更を即時反映させることができます。

○サンプルコード8:actorを用いた画像の並行処理

画像の並行処理は、特に大量の画像データを扱う場合には、非常に時間がかかる作業となります。

しかし、Swiftのactorを使用することで、これを効率的に行うことが可能です。

このコードでは、actorを用いて複数の画像を同時に並行して処理する例を表しています。

import UIKit

actor ImageProcessor {
    func processImage(image: UIImage) async -> UIImage {
        // ここで画像の加工処理を行う
        return image
    }
}

let images: [UIImage] = [...] // いくつかの画像データ
var processedImages: [UIImage] = []

let processor = ImageProcessor()

Task {
    for image in images {
        let processed = await processor.processImage(image: image)
        processedImages.append(processed)
    }
}

この例では、ImageProcessorというactorが画像の加工処理を非同期に行う機能を持っています。

そして、複数の画像データに対して、この加工処理を並行に実行することができます。

このコードを実行すると、複数の画像データが非同期に並行して加工され、加工後の画像データがprocessedImages配列に追加されます。

これにより、大量の画像データの加工を高速に行うことができます。

●actorを使う際の注意点と対処法

Swiftのactorは、スレッドセーフなコードを実現するための強力なツールです。

しかし、効果的に活用するためには、いくつかの注意点と対処法を理解する必要があります。

○データ競合の潜在的なリスク

actorが提供するスレッドセーフは非常に魅力的ですが、それでもデータ競合のリスクは完全には排除されません。

特に、複数のactorが同じリソースにアクセスしようとすると、意図しない結果を引き起こす可能性があります。

このコードでは、複数のactorが同時に同じリソースにアクセスしているシチュエーションを表しています。

この例では、BankAccountというactorが存在し、複数のactorから同時に預金の操作が行われています。

actor BankAccount {
    var balance: Int = 0

    func deposit(amount: Int) {
        balance += amount
    }

    func withdraw(amount: Int) {
        balance -= amount
    }
}

let account = BankAccount()

// actor1
Task {
    await account.deposit(amount: 1000)
}

// actor2
Task {
    await account.withdraw(amount: 500)
}

この例では、actor1とactor2が同時に預金の操作を行っています。

actorのメカニズムにより、これらの操作はスレッドセーフに実行されますが、期待する結果と異なる場合があるため注意が必要です。

○actorの適切なスコープ設定

actorを宣言する際には、そのスコープを適切に設定することが重要です。

スコープが広すぎると、必要以上にアクセス制御が行われ、パフォーマンスに影響を及ぼす可能性があります。

一方、スコープが狭すぎると、actorの恩恵を十分に受けられないことがあります。

例として、下記のコードでは、ShoppingCartというactorを宣言し、その中で複数のメソッドを持つことを表しています。

actor ShoppingCart {
    var items: [String] = []

    func addItem(item: String) {
        items.append(item)
    }

    func removeItem(item: String) {
        if let index = items.firstIndex(of: item) {
            items.remove(at: index)
        }
    }

    func getItems() -> [String] {
        return items
    }
}

このShoppingCart actorでは、商品の追加、削除、取得という3つのメソッドが定義されています。

ここでのポイントは、actorのスコープを適切に設定して、各メソッドの役割を明確にすることです。

○デッドロックの回避方法

actorを使用する際のもう一つの大きな問題点は、デッドロックです。

これは、複数のactorがお互いにリソースの利用を待ってしまい、システム全体が停止してしまう状態を指します。

この問題を回避するためには、actor間の依存関係を最小限に抑え、リソースのアクセス順序を明確にする必要があります。

下記のコードでは、ActorAとActorBが相互にリソースを要求している状態を表しています。

actor ActorA {
    var resourceA: Int = 0

    func accessResourceB(from actorB: ActorB) async {
        await actorB.useResourceB()
    }
}

actor ActorB {
    var resourceB: Int = 0

    func accessResourceA(from actorA: ActorA) async {
        await actorA.useResourceA()
    }

    func useResourceB() {
        resourceB += 1
    }
}

この例では、ActorAがActorBのリソースを、ActorBがActorAのリソースを要求しています。

このような相互依存の関係は、デッドロックのリスクを高めるため、回避することが推奨されます。

●actorのカスタマイズ方法

Swiftのactorはカスタマイズが可能です。

これにより、特定のニーズや要件に合わせてactorを適切に活用することができます。

○サンプルコード9:カスタムactorの属性を使った例

Swiftのactorにはカスタム属性があります。

これを利用することで、actorの動作や挙動を自分の目的に合わせて変更することができます。

// カスタム属性を持つactorの例
@CustomActorAttribute
actor CustomizedActor {
    var data: Int = 0

    func updateData(newValue: Int) {
        data = newValue
    }
}

// 使用例
let myActor = CustomizedActor()
Task {
    await myActor.updateData(newValue: 10)
}

このコードでは、@CustomActorAttributeという仮想のカスタム属性を使って、CustomizedActorというactorを定義しています。

この例では、dataという変数の値を更新するupdateDataメソッドを持っています。

実際の動作としては、このカスタム属性を使用することでactorの内部の処理や動作が変更されることを想定しています。

このようにしてコードを実行すると、CustomizedActorの挙動がカスタム属性に基づいて変更されることが期待されます。

○サンプルコード10:actorの継承と拡張

Swiftのactorはクラスとは異なり、直接的な継承はサポートされていません。

しかし、拡張(extensions)を利用してactorに新たな機能を追加することは可能です。

// 基本のactor
actor BaseActor {
    var value: Int = 0

    func increment() {
        value += 1
    }
}

// BaseActorの拡張
extension BaseActor {
    func decrement() {
        value -= 1
    }
}

// 使用例
let base = BaseActor()
Task {
    await base.increment()
    await base.decrement()
}

このコードでは、初めにBaseActorというactorを定義しています。

その後、BaseActorを拡張してdecrementメソッドを追加しています。

この方法を利用すると、既存のactorに新たな機能を付け加えることができます。

実行すると、BaseActorのインスタンスであるbaseincrementメソッドとdecrementメソッドの両方を使用でき、valueの値を増減させることができます。

●actorのデバッグとトラブルシューティング

Swiftの新機能であるactorは、多くの利点を持ちながらも、適切なデバッグとトラブルシューティングが求められる場面があります。

actorの動作が期待通りでない場合や、思わぬエラーが発生した際のデバッグ手法を効率よく行うための技術や、パフォーマンスチューニングの手法を詳しく紹介します。

○サンプルコード11:actorのデバッグテクニック

actorをデバッグする際の一般的なテクニックは、通常のSwiftコードとは異なる部分が存在します。

actor内部での非同期処理が適切に行われているか、actorのメソッドが期待通りに動作しているかを検証するためのサンプルコードを紹介します。

import Swift

// actorの定義
actor Counter {
    var value: Int = 0

    func increment() {
        value += 1
    }

    func getCurrentValue() -> Int {
        return value
    }
}

// actorインスタンスの作成
let counter = Counter()

Task {
    // メソッドの呼び出し
    await counter.increment()
    let currentValue = await counter.getCurrentValue()
    print("現在の値は \(currentValue) です。") // ここで1が出力されるはず
}

このコードではCounterというactorを使って、カウンターの値をインクリメントするコードを表しています。

この例ではincrementメソッドを使って値を1増やし、getCurrentValueメソッドで現在の値を取得しています。

上記のコードを実行すると、”現在の値は 1 です。”という出力が得られます。

○サンプルコード12:actorのパフォーマンスチューニング

actorを利用したアプリケーションのパフォーマンスを向上させるための方法には、特定のメソッドや処理を最適化するというアプローチがあります。

下記のサンプルコードでは、actor内での処理を効率よく行うための手法を表しています。

import Swift

// actorの定義
actor EfficientCounter {
    private var value: Int = 0

    func increment(by amount: Int) {
        for _ in 0..<amount {
            value += 1
        }
    }

    func getCurrentValue() -> Int {
        return value
    }
}

// actorインスタンスの作成
let efficientCounter = EfficientCounter()

Task {
    // メソッドの呼び出し
    await efficientCounter.increment(by: 1000)
    let currentValue = await efficientCounter.getCurrentValue()
    print("現在の値は \(currentValue) です。") // ここで1000が出力されるはず
}

このコードではEfficientCounterというactorを使って、一度のメソッド呼び出しでカウンターの値を指定された数だけ増やすコードを表しています。

この例ではincrement(by:)メソッドを使って値を1000増やしています。

上記のコードを実行すると、”現在の値は 1000 です。”という出力が得られます。

まとめ

Swiftの新機能であるactorは、非同期処理や同時実行のコーディングを簡単かつ安全に行うための重要なツールです。

今回の記事を通じて、actorの基本から詳細な使い方、デバッグのテクニック、そしてパフォーマンスチューニングの方法について学ぶことができたかと思います。

actorを適切に利用することで、従来の方法よりも効率的で安全なコードの実装が可能となります。

特に複数のスレッドやタスクが同時にアクセスするリソースを管理する際のスレッドセーフな操作が、actorを使用することで容易に実現できます。

しかし、その強力な機能を最大限に活用するためには、actorの持つ特性や動作をしっかりと理解し、適切なデバッグや最適化の手法を知ることが欠かせません。

今回紹介した内容を参考に、Swiftのactorを効果的に使用して、より高品質なアプリケーションの開発を進めてください。