Swiftでの並列処理の手法15選 – Japanシーモア

Swiftでの並列処理の手法15選

Swiftプログラムのスクリーンショットと並列処理のシンボルSwift
この記事は約23分で読めます。

 

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

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

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

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

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

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

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

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

はじめに

並列処理は、現代の多くのアプリケーションにおいて不可欠な技術となっています。

特にリアルタイムに近い反応速度が求められるアプリや、複数のタスクを同時に処理する必要があるアプリでは、並列処理の知識が重要です。

SwiftはiOSやmacOSなどのAppleプラットフォーム向けのプログラムを開発する際の主要な言語となっており、その中で並列処理を効果的に取り入れることが求められます。

●Swiftと並列処理の基本

○Swiftとは

SwiftはAppleが開発したプログラミング言語で、Objective-Cの後継として2014年に発表されました。

型安全性や高速性を持ちつつ、シンタックスが読みやすく、初心者にも取り組みやすい特性を持っています。

また、Playgroundなどの環境を利用することで、リアルタイムにコードの動作を確認しながら開発を進めることができるのも特徴の一つです。

○並列処理とは

並列処理とは、複数のプロセスやスレッドを同時に実行することで、タスクを高速に処理する手法のことを指します。

具体的には、CPUの複数のコアを活用したり、IO待ちの時間を無駄にしないための技術として利用されます。

並列処理は、プログラムのパフォーマンス向上に欠かせない技術となっています。

○なぜ並列処理が必要なのか

近年のCPUは、クロック速度の向上よりも、コア数の増加に重点を置いて進化しています。

このため、1つのタスクを迅速に処理するだけでなく、複数のタスクを同時に処理することが求められるようになりました。

また、ユーザーの体験を向上させるためにも、待ち時間を最小限に抑えるための並列処理が必要です。

例えば、ユーザーインターフェース(UI)をスムーズに動かしながら、バックグラウンドでデータを取得するような場面では、並列処理の知識が不可欠です。

●Swiftでの並列処理の手法

並列処理の手法は多岐にわたりますが、Swiftで主に使用される方法について紹介します。

中でも、Grand Central Dispatch(GCD)とOperationQueueは、Swiftでの並列処理を行う際の基盤となる技術です。

○Grand Central Dispatch(GCD)の概要

GCDは、Appleが提供している低レベルのAPIで、非同期のタスクをキューに入れて実行する機能を提供します。

これにより、シンプルな非同期処理から複雑な並列処理まで、幅広くカバーすることができます。

□サンプルコード1:基本的なGCDの使用方法

SwiftでGCDを使用する基本的な方法をサンプルコードで紹介します。

import Dispatch

// 非同期に実行するタスクを定義
DispatchQueue.global().async {
    print("非同期で実行されるタスク")
}

print("メインスレッドのタスク")

このコードでは、非同期で処理を行うタスクをDispatchQueue.global().asyncを使用してキューに追加しています。

この結果、メインスレッドはブロックされずに次のタスクに移行し、非同期のタスクもバックグラウンドで実行されます。

□サンプルコード2:非同期のタスク実行

非同期タスクをさらに詳しく見てみましょう。

下記のサンプルでは、非同期タスクの完了後にメインスレッドでUIの更新を行う例を表しています。

import Dispatch

DispatchQueue.global().async {
    // 非同期で行う処理
    print("非同期タスクの処理")

    // 処理完了後、メインスレッドでUI更新
    DispatchQueue.main.async {
        print("メインスレッドでのUI更新")
    }
}

このサンプルでは、DispatchQueue.main.asyncを使用して、非同期タスク完了後にメインスレッドでUIの更新を行っています。

□サンプルコード3:複数のタスクを並行実行

複数の非同期タスクを並行して実行する場合のサンプルコードを紹介します。

import Dispatch

let group = DispatchGroup()

for i in 1...3 {
    DispatchQueue.global().async(group: group) {
        print("タスク\(i)の実行")
    }
}

group.notify(queue: .main) {
    print("全てのタスクが完了")
}

このコードでは、DispatchGroupを使用して複数のタスクをグループ化し、全てのタスクが完了した際の通知を設定しています。

Swiftでの並列処理の手法15選

○OperationQueueの概要

OperationQueueは、Swiftで利用できる高レベルのAPIで、非同期操作のキューを管理するための強力なツールです。

GCDよりも柔軟性が高く、依存関係や優先度の設定など、細かなタスクの制御が可能です。

特に、複雑な非同期処理や順序付けられたタスクの実行に適しています。

□サンプルコード4:OperationQueueの基本的な使用方法

SwiftでのOperationQueueの基本的な使用方法をサンプルコードで紹介します。

let queue = OperationQueue()

queue.addOperation {
    print("非同期で実行されるタスク1")
}

queue.addOperation {
    print("非同期で実行されるタスク2")
}

このコードでは、OperationQueueを生成し、そのキューに非同期タスクを追加しています。

これにより、タスクが順序良く実行されます。

□サンプルコード5:Operationの依存関係の設定

Operation間の依存関係を設定して、一つのタスクが完了するまで別のタスクを待機させることができます。

let queue = OperationQueue()
let task1 = BlockOperation {
    print("タスク1の実行")
}

let task2 = BlockOperation {
    print("タスク2の実行")
}

task2.addDependency(task1)

queue.addOperations([task1, task2], waitUntilFinished: false)

このコードでは、task2task1の完了を待ってから実行されます。

□サンプルコード6:Operationの優先度設定

Operationには優先度を設定することができます。

これにより、特定のタスクを他のタスクよりも先に、または後に実行することが可能です。

let queue = OperationQueue()
let task1 = BlockOperation {
    print("タスク1の実行")
}

let task2 = BlockOperation {
    print("タスク2の実行")
}

task1.queuePriority = .high
task2.queuePriority = .low

queue.addOperations([task1, task2], waitUntilFinished: false)

このサンプルコードでは、task1task2よりも高い優先度を持っているため、先に実行される可能性が高くなります。

●Swiftでの並列処理の応用例

Swiftにおける並列処理は、単に複数のタスクを同時に実行するだけでなく、さまざまな応用的なシチュエーションに適用することができます。

ここでは、その具体的な応用例として、大量のデータ処理や画像の非同期ダウンロードについて解説します。

○サンプルコード7:大量のデータ処理

大量のデータを効率よく処理するために、並列処理を利用することが考えられます。

例えば、配列内の数値を非同期に加算するタスクを考えてみましょう。

import Dispatch

let numbers = Array(1...10000)
let queue = DispatchQueue.global()
var sum = 0

queue.async {
    for number in numbers {
        sum += number
    }
    print("合計: \(sum)")
}

このコードでは、1から10,000までの数値を含む配列numbersを作成し、非同期にその合計値を計算しています。

このような大量のデータの計算を高速に実行するために、非同期処理を利用すると、効果的です。

○サンプルコード8:画像の非同期ダウンロード

モバイルアプリケーションやウェブサービスなどでよく見られるのが、画像の非同期ダウンロードです。

下記のコードは、URLから画像を非同期にダウンロードするシンプルな例を表しています。

import UIKit

let imageUrl = URL(string: "https://example.com/sample.jpg")!
let imageView = UIImageView()

DispatchQueue.global().async {
    if let data = try? Data(contentsOf: imageUrl) {
        let image = UIImage(data: data)
        DispatchQueue.main.async {
            imageView.image = image
        }
    }
}

このコードは、指定されたURLから画像データを非同期にダウンロードし、ダウンロードが完了したらメインスレッドでUIImageViewにセットしています。

このように、画像の非同期ダウンロードを実現することで、ユーザー体験の向上が期待できます。

○サンプルコード9:データベースの非同期アクセス

データベースへのアクセスは、通常、時間がかかる操作の一つです。

特に大量のデータを取得したり、複雑なクエリを実行する場合、処理が完了するまでの待ち時間が長くなる可能性があります。

そうした場面で非同期処理を利用することで、アプリケーションの応答性を保ちつつ、バックグラウンドでデータベースの操作を行うことができます。

下記のコードは、SQLiteデータベースに非同期でアクセスするSwiftのサンプルコードです。

import SQLite3

let dbPath: String = "path_to_your_database.db"
var db: OpaquePointer?
var stmt: OpaquePointer?

if sqlite3_open(dbPath, &db) != SQLITE_OK {
    print("データベースのオープンに失敗しました。")
    return
}

let query = "SELECT * FROM your_table_name"

DispatchQueue.global().async {
    if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
        while sqlite3_step(stmt) == SQLITE_ROW {
            let id = sqlite3_column_int(stmt, 0)
            if let cString = sqlite3_column_text(stmt, 1) {
                let name = String(cString: cString)
                print("ID: \(id), 名前: \(name)")
            }
        }
    }
    sqlite3_finalize(stmt)
}
sqlite3_close(db)

このコードでは、SQLiteデータベースを非同期でオープンし、テーブルから全てのデータを取得しています。

DispatchQueue.global().asyncを使用して非同期にデータベースクエリを実行しており、メインスレッドをブロックせずにデータベースの操作が可能になっています。

○サンプルコード10:UI更新と並列処理の組み合わせ

SwiftでのUI更新は、メインスレッドで行う必要があります。

しかし、長時間かかるタスクとUIの更新を組み合わせる際は、並列処理を駆使して効率的に処理を行うことが求められます。

下記のサンプルコードは、非同期にデータ処理を行った後、その結果をメインスレッドでUIに表示する一例です。

import UIKit

let label = UILabel()

DispatchQueue.global().async {
    // 重たい処理のシミュレーション
    let result = "非同期でのデータ処理が完了しました。"

    DispatchQueue.main.async {
        label.text = result
    }
}

このコードでは、非同期に行われるデータ処理の後、得られた結果をDispatchQueue.main.asyncを使用してメインスレッドでUIのラベルに反映させています。

このように、非同期処理とUIの更新を適切に組み合わせることで、アプリケーションの応答性を向上させることができます。

○サンプルコード11:リソースの限定的なアクセス制御

多くのアプリケーションでは、一度に多数のスレッドやタスクがリソースにアクセスする場面があります。

その際、リソースの同時アクセスを制御することで、データの破損や不整合を防ぐ必要が出てきます。

Swiftでは、Semaphoreを使ってリソースへの同時アクセスを制限することができます。

下記のコードは、SwiftでSemaphoreを使用して、一度に2つのタスクだけがリソースにアクセスできるようにするサンプルコードです。

import Dispatch

let semaphore = DispatchSemaphore(value: 2) // 同時に2つのタスクだけがアクセスを許可

for i in 1...10 {
    DispatchQueue.global().async {
        semaphore.wait() // リソースの利用を待つ

        print("タスク\(i)がリソースを利用開始")
        sleep(2) // リソース利用のシミュレーション
        print("タスク\(i)がリソース利用終了")

        semaphore.signal() // リソースの利用が終了
    }
}

このコードを実行すると、最初の2つのタスクがリソースにアクセスします。

それらのタスクがリソースの利用を終了すると、次の2つのタスクがアクセスを開始します。

これにより、タスクの同時アクセスを2つに制限して、リソースへのアクセスを均等に行うことができます。

○サンプルコード12:特定のタスクをキャンセル

長時間実行されるタスクやリソースを多く消費するタスクがある場合、ユーザーの操作や条件に応じてそのタスクをキャンセルする必要が生じることがあります。

SwiftのOperationを使用すると、タスクのキャンセルが容易に行えます。

下記のサンプルコードは、Operationを使用して特定のタスクをキャンセルする方法を表しています。

import Foundation

class MyTask: Operation {
    let taskNumber: Int

    init(taskNumber: Int) {
        self.taskNumber = taskNumber
    }

    override func main() {
        if isCancelled {
            return
        }

        print("タスク\(taskNumber)実行開始")
        sleep(3)
        if isCancelled {
            return
        }
        print("タスク\(taskNumber)実行終了")
    }
}

let task1 = MyTask(taskNumber: 1)
let task2 = MyTask(taskNumber: 2)

let queue = OperationQueue()
queue.addOperations([task1, task2], waitUntilFinished: false)

sleep(1) // 一時停止

task1.cancel() // task1のキャンセル

このコードを実行すると、2つのタスクがキューに追加されます。

しかし、sleep(1)の後、task1がキャンセルされるため、そのタスクの実行が中止されます。

一方、task2は通常通り実行が完了します。

○サンプルコード13:進捗報告と完了ハンドラの利用

多くのアプリケーションでは、タスクの進行状況をリアルタイムでユーザーに伝えることが必要です。

また、タスクが完了したときに特定の処理を実行する場面も頻繁にあります。

Swiftでは、Progress オブジェクトと完了ハンドラを使用して、これらの要件を簡単に実現することができます。

まず、Progress オブジェクトを使用して進捗を報告する方法について解説します。

import Foundation

// 進捗オブジェクトの生成
let progress = Progress(totalUnitCount: 10)

// 進捗の監視
progress.addObserver(self, forKeyPath: #keyPath(Progress.fractionCompleted), options: .new, context: nil)

DispatchQueue.global().async {
    for i in 1...10 {
        if progress.isCancelled { break }  // キャンセルチェック

        sleep(1)  // 何らかの処理を模倣
        progress.completedUnitCount = Int64(i)  // 進捗の更新
    }
}

// 進捗の監視時の処理
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == #keyPath(Progress.fractionCompleted) {
        DispatchQueue.main.async {
            print("進捗: \(progress.fractionCompleted * 100) %")
        }
    }
}

このコードでは、10のタスクが連続で実行されると想定しています。

各タスクが完了する度に、progress.completedUnitCountが更新され、進捗が報告されます。

次に、非同期タスクの完了時にハンドラを使用する方法を紹介します。

import Foundation

func performTask(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        sleep(3)  // 何らかの処理を模倣
        completion("タスク完了")  // 完了ハンドラの呼び出し
    }
}

performTask { (message) in
    print(message)
}

このコードでは、performTask関数が非同期にタスクを実行し、その完了時に指定されたハンドラを呼び出すようになっています。

○サンプルコード14:GCDとOperationQueueの組み合わせ

Swiftの並列処理において、GCDとOperationQueueはそれぞれの利点を持っています。

これらを適切に組み合わせることで、より高度な並列処理を実現することができます。

例として、GCDを用いて非同期に処理を実行し、その結果をOperationQueueで順序よく処理する方法を見てみましょう。

import Dispatch

let dispatchQueue = DispatchQueue.global()
let operationQueue = OperationQueue()

dispatchQueue.async {
    let result = "非同期の結果"  // 何らかの非同期処理を模倣

    operationQueue.addOperation {
        print("Operation1: \(result)")
    }

    operationQueue.addOperation {
        print("Operation2: \(result)を更に処理")
    }
}

このコードを実行すると、GCDの非同期タスクが実行された後、OperationQueueで2つのオペレーションが順番に実行されます。

○サンプルコード15:ネストした並列処理の実現

アプリケーションの要件に応じて、複数の並列処理を組み合わせることが求められることがあります。Swiftでは、これを簡単に実現することができます。

下記のサンプルコードは、非同期処理の中でさらに複数の並列処理を行う方法を表しています。

import Dispatch

let outerQueue = DispatchQueue.global()
let innerQueue = DispatchQueue.concurrentPerform(iterations: 5) { index in
    print("内部タスク: \(index)")
}

outerQueue.async {
    print("外部タスク開始")
    innerQueue.sync {
        print("内部タスク終了")
    }
    print("外部タスク終了")
}

このコードでは、outerQueueで非同期に処理を開始し、その中でinnerQueueで5つの並列タスクを実行しています。

●並列処理の注意点と対処法

並列処理を行う際には、いくつかの注意点や問題が発生する可能性があります。

それらの問題を効果的に対処する方法を知ることで、アプリケーションの品質を向上させることができます。

○デッドロックとは

デッドロックとは、2つ以上のタスクがお互いに必要とするリソースを持ち、どちらも先に進めなくなる状態を指します。

これが発生すると、プログラムは停止してしまい、動作が進行しなくなります。

例えば、タスクAがリソース1を保持していてリソース2を要求し、同時にタスクBがリソース2を保持してリソース1を要求すると、デッドロックが発生します。

let lock1 = NSLock()
let lock2 = NSLock()

// タスクA
DispatchQueue.global().async {
    lock1.lock()
    sleep(1)
    lock2.lock()
    print("タスクA完了")
    lock2.unlock()
    lock1.unlock()
}

// タスクB
DispatchQueue.global().async {
    lock2.lock()
    sleep(1)
    lock1.lock()
    print("タスクB完了")
    lock1.unlock()
    lock2.unlock()
}

このコードでは、タスクAとタスクBがお互いにリソースを要求し合っており、デッドロックが発生します。

どちらのタスクも”完了”というメッセージを出力することはできません。

○リソースの競合と解決策

複数のタスクが同時に1つのリソースにアクセスしようとすると、リソースの競合が発生する可能性があります。

これを避けるためには、排他制御を実装する必要があります。

排他制御は、一度に1つのタスクだけがリソースにアクセスできるように制御することを意味します。

Swiftでは、NSLockDispatchSemaphoreなどを使用して排他制御を実現することができます。

let lock = NSLock()
var data: Int = 0

// 排他制御を使用してリソースにアクセス
DispatchQueue.global().async {
    lock.lock()
    data += 1
    lock.unlock()
}

このコードでは、lockを使用してdataへのアクセスを排他制御しています。

これにより、同時に複数のタスクがdataを変更することはありません。

○非同期処理のエラーハンドリング

非同期処理の中でエラーが発生した場合、適切にエラーハンドリングを行うことが重要です。

Swiftでは、Result型を使用して、成功の結果やエラーを取り扱うことができます。

enum CustomError: Error {
    case someError
}

func asyncFunction(completion: @escaping (Result<String, CustomError>) -> Void) {
    DispatchQueue.global().async {
        let isSuccess = Bool.random()
        if isSuccess {
            completion(.success("成功の結果"))
        } else {
            completion(.failure(.someError))
        }
    }
}

asyncFunction { result in
    switch result {
    case .success(let message):
        print(message)
    case .failure(let error):
        switch error {
        case .someError:
            print("エラーが発生しました。")
        }
    }
}

このコードでは、非同期処理の中でランダムに成功またはエラーを生成しています。

エラーハンドリングは、コールバック関数の中でResult型を使用して行われます。

●並列処理のカスタマイズ方法

Swiftでの並列処理をより効果的に利用するためには、カスタマイズが必要になる場面が多々あります。

ここでは、GCDのカスタムキューの作成方法やOperationのカスタマイズ方法を中心に解説していきます。

○GCDのカスタムキューの作成

Grand Central Dispatch(GCD)は、非常に強力な並列処理のフレームワークです。

デフォルトで提供されるキューの他に、自分自身でキューを作成し、そのキューにタスクを追加することができます。

// カスタムの非同期キューを作成
let customQueue = DispatchQueue(label: "com.example.customQueue")

// カスタムキューにタスクを追加
customQueue.async {
    print("カスタムキューで実行されるタスク")
}

このコードでは、カスタムキューを作成しています。

そして、そのキューに非同期のタスクを追加して実行しています。

タスクが実行されると、”カスタムキューで実行されるタスク”というメッセージが出力されます。

○Operationのカスタマイズ

Operationは、より詳細な制御が必要な場合や、複数のタスクの依存関係を設定する際に便利なクラスです。

このOperationをカスタマイズすることで、より複雑なタスクの実行や制御を行うことができます。

// カスタムOperationの定義
class CustomOperation: Operation {
    override func main() {
        print("カスタムOperationで実行されるタスク")
    }
}

// カスタムOperationのインスタンスを作成
let operation = CustomOperation()

// OperationQueueに追加して実行
let operationQueue = OperationQueue()
operationQueue.addOperation(operation)

このコードでは、カスタムのOperationクラスを定義しています。

その後、そのクラスのインスタンスを作成し、OperationQueueに追加して実行しています。

実行結果として、”カスタムOperationで実行されるタスク”というメッセージが出力されます。

まとめ

Swiftでの並列処理は、アプリケーションのパフォーマンス向上や効率的なリソースの活用に欠かせない要素です。

本記事では、Swiftでの並列処理を効果的に実現するための具体的な手法を15のサンプルコードとともに詳細に紹介しました。

初心者から中級者までの方々が、Grand Central Dispatch(GCD)やOperationQueueといったフレームワークを効果的に使用して、非同期処理や複数のタスクの並行実行などの技術をマスターするための情報を提供しました。

さらに、並列処理の際の注意点や、カスタマイズ方法についても詳しく解説しました。

Swiftを使用してアプリケーションの開発を行う際、並列処理は避けて通れない技術です。

この記事が、読者の皆様のSwiftにおける並列処理の理解を深める手助けとなれば幸いです。

最後まで読んでいただき、ありがとうございました。