Swiftで学ぶ同期処理の10選!初心者も理解できる徹底ガイド

Swiftでの同期処理を学ぶ初心者のためのイラストSwift
この記事は約21分で読めます。

 

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

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

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

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

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

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

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

はじめに

Swiftを学び始めると、どんな言語を学ぶ場面でも出くわす「同期処理」の存在に気づくことでしょう。

この記事を読めば、Swiftでの同期処理を理解し、実装することができるようになります。

Swiftの同期処理の基本から、実際にプログラムに組み込むまでの手順を、初心者の方でもわかりやすく説明します。

また、サンプルコードも交えて具体的にどのように実装するのかを示していきます。

●Swiftの同期処理とは

同期処理とは、一つのタスクが完了するまで次のタスクが待たされ、一つ一つのタスクが順番に実行される処理のことを指します。

これに対し、非同期処理は複数のタスクが同時に並行して実行されるものです。

○同期処理の基本概念

Swiftでの同期処理は、特にマルチスレッドの環境や、ネットワーク通信、ファイルの読み書きなど、一定の時間がかかるタスクを行う際によく利用されます。

同期処理を利用することで、一つのタスクが完了するまで次のタスクを待つことができ、順序通りに処理を進めることができます。

このコードでは、配列の要素を順番に表示する同期処理を表しています。

この例では、for文を使用して配列の要素を一つずつ取り出し、コンソールに表示しています。

let items = ["apple", "banana", "cherry"]

for item in items {
    print(item)
    sleep(1) // 1秒待機
}

このコードを実行すると、配列の各要素が1秒ごとに順番に表示されます。

このように、同期処理では処理が順番に実行されることが確認できます。

○非同期処理との違い

非同期処理は、前述の通り複数のタスクを同時に実行することができます。

これにより、一つのタスクが終わるのを待たずに次のタスクを進めることができ、効率的な処理が期待できます。

しかし、非同期処理を扱う際は、タスクの終了のタイミングや、複数のタスクがアクセスするリソースの管理など、考慮すべき点が増えます。

Swiftでは、非同期処理を実現するためにGCD(Grand Central Dispatch)やOperationQueueといったライブラリやフレームワークが提供されています。

これらのツールを利用することで、同期処理と非同期処理を組み合わせて、効率的なプログラムを実現することができます。

このコードでは、非同期処理で複数のタスクを同時に実行する例を表しています。

この例では、DispatchQueueを使用して非同期処理を行い、複数のタスクを並行して実行しています。

let queue = DispatchQueue.global(qos: .default)

queue.async {
    for _ in 1...3 {
        print("Task 1")
        sleep(2)
    }
}

queue.async {
    for _ in 1...3 {
        print("Task 2")
        sleep(1)
    }
}

このコードを実行すると、「Task 1」と「Task 2」が交互に表示されることが確認できます。

このように、非同期処理では複数のタスクが同時に実行されることが確認できます。

●Swiftでの同期処理の使い方

Swiftでの同期処理の実装方法は、初めての方には難しく感じるかもしれませんが、基本的な考え方を理解すればスムーズにコードを書くことができます。

ここでは、Swiftでの同期処理の基本的な使い方から、具体的なサンプルコードを交えて解説していきます。

○サンプルコード1:基本的な同期処理の実装

Swiftで同期処理を実装する基本的な方法は、一つのタスクが完了するまで次のタスクが待たされ、一つ一つのタスクが順番に実行されるようにすることです。

このコードでは、Swiftの基本的な同期処理を使って数値を順番に表示しています。

この例では、for文を利用して、0から4までの数字を順番に表示しています。

for number in 0...4 {
    print(number)
    sleep(1) // 1秒待機
}

実行すると、0から4までの数字が1秒間隔で順番に表示されます。

○サンプルコード2:同期処理を利用したデータの取得

同期処理は、特にデータベースからのデータ取得やAPIからのレスポンス取得など、外部リソースとの通信においてよく用いられます。

下記のサンプルコードでは、外部のAPIからデータを取得する際の同期処理の例を表しています。

ただし、このサンプルは実際に外部のAPIと通信するものではなく、データの取得を模したものです。

func fetchData() -> String {
    sleep(3) // 3秒間の待機を模倣
    return "Fetched Data!"
}

let data = fetchData()
print(data)

このコードでは、fetchData関数を呼び出してデータを取得した後、取得したデータをコンソールに表示しています。

実行すると、「Fetched Data!」という文字列が3秒後に表示されます。

このように、fetchData関数が完了するまで、次のprint関数の実行が待たされることがわかります。

○サンプルコード3:同期処理でのエラーハンドリング

同期処理を使用してプログラムを実行する際、想定外のエラーが発生する可能性が常にあります。

Swiftには、このようなエラーを効果的にハンドリングするための仕組みが提供されています。

ここでは、Swiftでの同期処理中のエラーハンドリング方法を具体的なサンプルコードを用いて解説します。

まず、下記のサンプルコードは、指定された数値が10未満の場合にエラーをスローする簡単な関数を表しています。

この例では、checkNumber関数を使用して数値をチェックし、エラーをスローしてエラーメッセージを表示しています。

enum NumberError: Error {
    case tooSmall
}

func checkNumber(_ number: Int) throws {
    if number < 10 {
        throw NumberError.tooSmall
    }
}

do {
    try checkNumber(5)
} catch NumberError.tooSmall {
    print("エラー: 数値が10未満です。")
}

このコードを実行すると、”エラー: 数値が10未満です。”というエラーメッセージが表示されます。

do-catchブロックを使用することで、checkNumber関数でスローされたエラーをキャッチして適切な処理を行うことができます。

○サンプルコード4:同期処理でのループ処理

同期処理中にループを使用する際の注意点として、ループの中で長時間の処理が発生すると、その間他の処理がブロックされるため、アプリケーションのパフォーマンスが低下する可能性があります。

しかし、適切な設計と最適化を行うことでこの問題を回避することができます。

下記のサンプルコードは、1から50までの数値を順番に表示する簡単な同期処理のループを表しています。

この例では、for文を使用して数値を表示し、各数値の表示後に0.1秒の待機時間を設けています。

for i in 1...50 {
    print(i)
    sleep(UInt32(0.1))
}

実行すると、1から50までの数値が0.1秒間隔で順番に表示されます。

このコードはシンプルですが、ループ中の待機時間を適切に設定することで、他の処理への影響を最小限に抑えることができます。

○サンプルコード5:同期処理を利用したUI更新

Swiftのアプリケーション開発において、UIの更新は非常に重要です。しかし、同期処理を使用してUIを更新する場合、注意が必要です。

UIの更新はメインスレッドで行う必要があり、バックグラウンドスレッドでUIの更新を行うとアプリケーションがクラッシュする可能性があります。

このコードでは、DispatchQueueを使ってメインスレッド上でテキストラベルの内容を更新する方法を表しています。

この例では、updateTextLabel関数を使用してラベルのテキストを更新しています。

import UIKit

let label = UILabel()

func updateTextLabel() {
    DispatchQueue.main.async {
        label.text = "テキストを更新しました。"
    }
}

updateTextLabel()

このコードを実行すると、ラベルのテキストが”テキストを更新しました。”に更新されます。

DispatchQueue.main.asyncを使用することで、メインスレッド上でのUI更新を保証しています。

●Swiftでの同期処理の応用例

Swiftの同期処理は、基本的な実装だけでなく、様々なシチュエーションでの応用が可能です。

特に、データの取得やUIの更新などの実用的なシチュエーションでの同期処理の利用は、アプリケーションの効率とユーザー体験の向上に寄与します。

ここでは、Swiftでの同期処理の応用例として、いくつかの具体的なシチュエーションを取り上げ、サンプルコードとともに解説していきます。

○サンプルコード6:同期処理を利用した画像のダウンロード

画像のダウンロードは、多くのアプリケーションで頻繁に行われる操作の一つです。

下記のコードでは、URLを指定して画像をダウンロードし、それをImageViewに表示しています。

import UIKit

func downloadImage(from url: URL) -> UIImage? {
    if let data = try? Data(contentsOf: url) {
        return UIImage(data: data)
    }
    return nil
}

let imageUrl = URL(string: "https://example.com/sample.jpg")!
if let image = downloadImage(from: imageUrl) {
    let imageView = UIImageView(image: image)
}

このコードではdownloadImage関数を使って指定されたURLから画像をダウンロードしています。

ダウンロードが成功すれば、UIImageのインスタンスが返され、それをUIImageViewにセットして表示することができます。

○サンプルコード7:同期処理を用いたデータベース操作

データベース操作はアプリケーション開発の中心的な要素の一つであり、これを同期処理で行うことで、データの整合性を保ちつつ効率的な処理が可能となります。

下記のコードは、SQLiteを使用してデータベースから情報を取得するシンプルな例を表しています。

import SQLite3

let dbPath: String = "path_to_database"
var db: OpaquePointer? = nil

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

let query = "SELECT name FROM users WHERE id = 1"
var statement: OpaquePointer? = nil

if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
    while sqlite3_step(statement) == SQLITE_ROW {
        let name = String(cString: sqlite3_column_text(statement, 0))
        print("ユーザー名: \(name)")
    }
}

sqlite3_finalize(statement)
sqlite3_close(db)

このコードでは、指定されたパスのデータベースを開き、SQLクエリを実行してユーザー名を取得しています。

取得したユーザー名は、コンソールに表示されます。

同期処理を用いることで、データベース操作中に他の処理が介入することなく、安全にデータの取得や更新が行えます。

○サンプルコード8:同期処理を活用したアニメーション制御

アニメーションはユーザーインターフェイスの一部としてアプリケーションで頻繁に用いられます。

同期処理を活用することで、アニメーションの開始と終了、またその間の動作をより制御しやすくなります。

ここでは、SwiftでUIViewのアニメーションを同期処理を用いて順に実行する例を紹介します。

import UIKit

class AnimationViewController: UIViewController {

    let animatedView = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()

        animatedView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
        animatedView.backgroundColor = .red
        view.addSubview(animatedView)

        DispatchQueue.main.sync {
            startSequentialAnimation()
        }
    }

    func startSequentialAnimation() {
        UIView.animate(withDuration: 1.0, animations: {
            self.animatedView.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
        }) { (completed) in
            if completed {
                UIView.animate(withDuration: 1.0) {
                    self.animatedView.backgroundColor = .blue
                }
            }
        }
    }
}

このコードでは、startSequentialAnimation関数にて、animatedViewの位置を変更するアニメーションを1秒間実行した後、背景色を青に変更するアニメーションを順に実行しています。

同期処理を活用することで、前のアニメーションが完了したことを確認し、次のアニメーションをスムーズに実行することが可能です。

○サンプルコード9:同期処理での多重実行

Swiftの同期処理を使用すると、複数の処理を順番に実行することも可能です。

この手法は、特定のタスクが終了するのを待ってから次のタスクを開始する場面などで非常に有効です。

ここでは、3つのタスクを順番に実行するシンプルな例を紹介します。

import Foundation

let serialQueue = DispatchQueue(label: "com.example.serialQueue")

serialQueue.sync {
    task1()
    task2()
    task3()
}

func task1() {
    print("タスク1が完了しました")
    sleep(1)
}

func task2() {
    print("タスク2が完了しました")
    sleep(1)
}

func task3() {
    print("タスク3が完了しました")
}

このコードではtask1task2、そしてtask3という3つのタスクを、指定された順番に実行しています。

sleep(1)を使って各タスクの実行に1秒の遅延を持たせているため、出力結果は「タスク1が完了しました」、「タスク2が完了しました」、「タスク3が完了しました」と順に表示されます。

○サンプルコード10:同期処理と非同期処理の組み合わせ

Swiftでは、同期処理と非同期処理を組み合わせて使用することも可能です。

これにより、特定のタスクは順序通りに実行しつつ、他のタスクはバックグラウンドで並行して実行するといった複雑なシナリオを実現することができます。

ここでは、この組み合わせを用いた例を紹介します。

import Foundation

let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

concurrentQueue.async {
    taskA()
}

concurrentQueue.sync {
    taskB()
}

func taskA() {
    print("タスクAを非同期で開始")
    sleep(2)
    print("タスクAが完了しました")
}

func taskB() {
    print("タスクBを同期で開始")
    sleep(1)
    print("タスクBが完了しました")
}

このコードでは、taskAは非同期処理で、taskBは同期処理で実行されます。

そのため、出力結果は「タスクAを非同期で開始」、「タスクBを同期で開始」、「タスクBが完了しました」、そして「タスクAが完了しました」という順番で表示されるでしょう。

●同期処理の注意点と対処法

同期処理を効果的に使用するためには、いくつかの注意点と対処法を押さえておくことが重要です。

それぞれのポイントについて、具体例をもとに詳細に解説します。

○デッドロックの問題と解決方法

同期処理を行う際、デッドロックは非常に避けたい問題の一つです。

デッドロックは、複数のスレッドが互いに他のスレッドの処理が終了するのを待ってしまい、結果としてどのスレッドも処理を進められなくなってしまう現象です。

下記のサンプルコードは、mainスレッド上で同期処理を実行しようとしてデッドロックが発生する例です。

import Foundation

let queue = DispatchQueue.main

queue.sync {
    print("このコードはデッドロックを引き起こします")
}

上記のコードでは、mainスレッド上でqueue.syncを呼び出しているため、デッドロックが発生します。

mainスレッドはUIの更新などで使用されるため、これをブロックしてしまうとアプリがフリーズしてしまいます。

デッドロックを避けるための一般的な対処法は、メインスレッドでの同期処理を避け、非同期処理を利用するか、他のスレッドで同期処理を行う方法です。

下記のコードは、非同期処理を使用してメインスレッドをブロックせずに処理を行う例です。

import Foundation

let queue = DispatchQueue.global()

queue.async {
    print("非同期処理なので、デッドロックは発生しません")
}

こちらのコードでは、グローバルキューを使用して非同期処理を行っているため、デッドロックを回遍でき、アプリがフリーズすることなく、コンソールにメッセージが表示されます。

○パフォーマンスへの影響

同期処理はその性質上、実行中のスレッドをブロックしてしまいます。

これがパフォーマンスに悪影響を与える場合があるため、適切な使用と最適化が求められます。

特に、UIを担当するメインスレッドでの同期処理は、ユーザー体験に直結するため注意が必要です。

下記のコードは、非効率な同期処理の例です。

import UIKit

DispatchQueue.main.sync {
    // 重い処理
    for _ in 0..<10000 {
        print("重い処理を行っています")
    }
}

このコードはメインスレッドで重い処理を同期的に行っており、これによりUIの更新が停滞してしまう可能性が高いです。

この問題を解決するためには、重い処理をバックグラウンドスレッドで実行するという方法があります。

import UIKit

DispatchQueue.global().async {
    // 重い処理
    for _ in 0..<10000 {
        print("重い処理を行っています")
    }
}

この修正により、”重い処理を行っています”というメッセージがコンソールに表示されつつも、メインスレッドはブロックされず、UIの更新やユーザー入力のレスポンスが継続されます。

○エラーハンドリングのベストプラクティス

同期処理においても、エラーハンドリングは重要な要素です。

エラーが発生した際に、それを適切にキャッチし、ユーザーに通知するか、ログに記録するなどの対処を行う必要があります。

下記のコードは、エラーハンドリングを取り入れた同期処理の例です。

import Foundation

enum MyError: Error {
    case somethingWentWrong
}

func riskyTask() throws {
    // 何らかのリスクのあるタスク
    throw MyError.somethingWentWrong
}

let queue = DispatchQueue.global()

queue.sync {
    do {
        try riskyTask()
    } catch {
        print("エラーが発生しました: \(error)")
    }
}

このコードでは、riskyTask関数内で何らかの理由でエラーが発生し、それをcatchブロックでキャッチしています。

エラーハンドリングを適切に行うことで、エラーの原因を特定しやすくなり、アプリの品質を向上させることができます。

●同期処理のカスタマイズ方法

Swiftでの同期処理は非常に柔軟性が高く、多様なカスタマイズが可能です。

ここでは、カスタムエラーの作成や同期処理の拡張方法、さらなる最適化の方法について、具体的なサンプルコードとともに詳細に説明します。

○カスタムエラーの作成

エラーハンドリングは、アプリ開発において非常に重要な部分です。

Swiftでは、カスタムエラーを簡単に作成することができます。

このカスタムエラーを使用することで、特定のエラーケースに応じた処理を実装することができます。

このコードでは、MySyncErrorという独自のエラー型を定義しています。

カスタムエラーを使用することで、特定のエラーシチュエーションを明確に識別し、それに応じた処理を行うことができます。

enum MySyncError: Error {
    case invalidData
    case timeout
    case unknown
}

func performTask(data: String?) throws {
    guard let _ = data else {
        throw MySyncError.invalidData
    }
    // その他のタスク処理...
}

上記のサンプルコードでは、データがnilの場合にinvalidDataエラーをスローしています。

このようなカスタムエラーを利用することで、エラーの原因を具体的に識別し、それに応じたエラーハンドリングが可能となります。

○同期処理の拡張と最適化

同期処理のカスタマイズとして、処理の拡張や最適化も考えられます。

これにより、アプリのパフォーマンスや応答性を向上させることが期待できます。

例として、特定のタスクが一定時間内に完了しなかった場合にタイムアウトとして処理を中断する仕組みを考えます。

下記のサンプルコードでは、withTimeout関数を使用して、指定されたタイムアウト時間内に処理が完了しない場合、タイムアウトエラーをスローします。

func withTimeout(_ seconds: Int, task: () throws -> Void) rethrows {
    let semaphore = DispatchSemaphore(value: 0)
    DispatchQueue.global().async {
        do {
            try task()
        } catch {
            // エラーハンドリング
            print("タスク内でエラーが発生しました: \(error)")
        }
        semaphore.signal()
    }

    if semaphore.wait(timeout: .now() + .seconds(seconds)) == .timedOut {
        throw MySyncError.timeout
    }
}

このコードでは、DispatchSemaphoreを使ってタスクの完了を待機しています。

指定された時間内にタスクが完了しない場合、timeoutエラーをスローします。

例えば、5秒以内に完了することが期待されるタスクがあれば、次のようにwithTimeout関数を使用して実行します。

do {
    try withTimeout(5) {
        // 5秒以上かかる可能性のあるタスク
        sleep(10)
    }
} catch MySyncError.timeout {
    print("タスクがタイムアウトしました")
}

この例では、sleep(10)という10秒間のスリープ処理が含まれているため、「タスクがタイムアウトしました」というメッセージがコンソールに表示されます。

まとめ

Swiftにおける同期処理は、アプリ開発の基盤となる重要な要素です。

この記事では、同期処理の基本的な概念から、実装方法、応用例、さらにはカスタマイズの方法まで、幅広く詳細に解説しました。

初心者の方でも理解しやすいように、具体的なサンプルコードを多数取り上げ、その実行結果や背景にある仕組みも丁寧に説明してきました。

特に、カスタムエラーの作成や同期処理の拡張と最適化の部分では、アプリケーションの品質をさらに向上させるためのテクニックを紹介しました。

しかし、技術は日々進化しています。

今回学んだ知識をベースに、さらなる最新の情報や技術を学び、アプリ開発のスキルを磨いていくことが重要です。

Swiftでの同期処理を理解し、適切に利用することで、ユーザーエクスペリエンスの高いアプリを作成することが可能となります。

この記事が、皆様のSwiftプログラミングの学びの一助となれば幸いです。

今後の開発活動に、ぜひともお役立てください。