LoginSignup
4
2

俺でもわかるSwift Concurrency

Last updated at Posted at 2023-12-22

この記事はCA Tech Lounge Advent Calendar 2023の22日目の記事です。

はじめに

こんにちは!大学三年生のFJ2123と申します。
大学で物流や情報について学ぶ傍ら、CA Tech Loungeには今年の5月から、iOSエンジニアとして参加しており、日々メンバーの方々と交流しながら開発に励んでいます。
せっかくラウンジのアドカレの枠をいただけたので、今回はiOSエンジニアとしてかつ、他職種の方にもイメージだけでも掴んでいただけるように、そこはかとなくSwift Concurrencyについてまとめていきたいと思います。

記事内に間違いがあった際はお知らせいただければ幸いです。

そもそもConcurrencyって何?

ConcurrencyはSwift5.5(2021年6月頃) から登場した言語機能です。
具体的には以下の2つを従来と比べて、簡潔&安全に記述することができます。

  1. 非同期処理
  2. 並行処理

非同期処理

Swiftでは非同期処理をクロージャーのコールバックで実装していますが、可読性が下がることやエラーハンドリングが難しいという問題があります。実際に、ネットワークリクエストを行い、取得したデータを処理するコードを見て確認してみましょう。

クロージャーのコールバックを使用した場合

import Foundation

// コールバックでデータをフェッチ
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    guard let url = URL(string: "https://example.com/data") else {
        completion(.failure(URLError(.badURL)))
        return
    }
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(URLError(.badServerResponse)))
            return
        }
        completion(.success(data))
    }
    task.resume()
}

// データの処理
func processFetchedData() {
    fetchData { result in
        switch result {
        case .success(let data):
            // データを処理
            // ...
        case .failure(let error):
            // エラーハンドリング
            print("Error fetching data: \(error)")
        }
    }
}

// 呼び出し
processFetchedData()

Concurrencyを使用した場合

import Foundation

// 非同期関数でデータをフェッチ
func fetchData() async throws -> Data {
    guard let url = URL(string: "https://example.com/data") else {
        throw URLError(.badURL)
    }
    
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

// データの処理
func processFetchedData() async {
    do {
        let data = try await fetchData()
        // データを処理
        // ...
    } catch {
        // エラーハンドリング
        print("Error fetching data: \(error)")
    }
}

// 呼び出し
Task {
    await processFetchedData()
}

上記の関数はそれぞれ同じ処理を行っていますが、関数fetchdataを見ると差は一目瞭然です。

下の抜き出しを見て一緒に確認していきましょう。

まず、コールバックで実装した場合は処理が以下のように行われています。
①taskインスタンスの取得
②リクエストの実行
③コールバック

下の抜粋に対応する番号を振ったのですが、処理の実行順序が①上、②下、③真ん中と分かれているので、どの順番で実行されるかを把握しづらいという問題が発生しています。

一方で、Concurrencyを用いて記述した際は、処理の順序が追いやすくなっていますね!

//コールバック
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    //①
    guard let url = URL(string: "https://example.com/data") else {
        completion(.failure(URLError(.badURL)))
        return
    }
    //③
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(URLError(.badServerResponse)))
            return
        }
        completion(.success(data))
    }
    //②
    task.resume()
}
//Concurrency
func fetchData() async throws -> Data {
    guard let url = URL(string: "https://example.com/data") else {
        throw URLError(.badURL)
    }
    
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

並行処理

続けて、並行処理についても実際のコードを見て比較していきましょう

クロージャーのコールバックを使用した場合

import Foundation

func fetchData(url: URL, completion: @escaping (Result<Data, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data else {
            completion(.failure(URLError(.badServerResponse)))
            return
        }
        completion(.success(data))
    }
    task.resume()
}

func processMultipleRequests(urls: [URL]) {
    let group = DispatchGroup()
    
    for url in urls {
        group.enter()
        fetchData(url: url) { result in
            defer { group.leave() }
            
            switch result {
            case .success(let data):
                // データを処理
                break
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    }
    
    group.notify(queue: .main) {
        print("All requests completed")
    }
}

// 使用例
let urlStrings = ["https://example.com/data1", "https://example.com/data2"]
let urls = urlStrings.compactMap { URL(string: $0) }
processMultipleRequests(urls: urls)


Concurrencyを使用した場合

import Foundation

func fetchData(url: URL) async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}

func processMultipleRequests(urls: [URL]) async {
    do {
        for url in urls {
            let data = try await fetchData(url: url)
            // データを処理
        }
    } catch {
        // エラーハンドリング
        print("Error: \(error)")
    }
}

// 使用例
Task {
    let urlStrings = ["https://example.com/data1", "https://example.com/data2"]
    let urls = urlStrings.compactMap { URL(string: $0) }
    await processMultipleRequests(urls: urls)
}

今回注目していただきたいのは、関数processMultipleRequestsです(下に抜粋)
やはりConcurrencyを使用した方が簡潔に記述できることがわかりますね🙌
また、ネストが深くならず可読性が上がることによってバグの発生も防ぐことができそうです👍

//コールバック
func processMultipleRequests(urls: [URL]) {
    let group = DispatchGroup()
    
    for url in urls {
        group.enter()
        fetchData(url: url) { result in
            defer { group.leave() }
            
            switch result {
            case .success(let data):
                // データを処理
                break
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    }
    
    group.notify(queue: .main) {
        print("All requests completed")
    }
}
//Concurrency
func processMultipleRequests(urls: [URL]) async {
    do {
        for url in urls {
            let data = try await fetchData(url: url)
            // データを処理
        }
    } catch {
        // エラーハンドリング
        print("Error: \(error)")
    }
}

キーワードの解説

さて、ここまで実際のコードを交えながらConcurrencyの利点について説明してきましたがConcurrencyを用いて記述する際に以下のキーワードがあったことはお気づきでしょうか?

  • async/await
  • throws
  • Task

これらのキーワードについて解説していきます!

async/await

関数の定義にasyncをつけると、その関数を非同期関数として定義することができます。
定義した非同期関数を呼び出すのにはawaitをつける必要があります。awaitをつけないとコンパイルエラーになるので注意です。(言い換えれば、非同期関数を同期的に呼び出すことをコンパイラが防いでくれるということでもあります。)

throws

throwsキーワードは、名前の通り関数がエラーを「投げる」ことを示唆するものになります。
これを使って定義された関数は、実行中に何らかの理由で失敗するとエラーを生成し、それを関数の呼び出し元に伝えることが可能になります。

具体的には以下のように、do-catch構文でエラーハンドリングを行うことができます。

func fetchData() async throws -> Data {
    // 非同期処理
}

Task {
    do {
        // データの処理
        let data = try await fetchData()
    } catch {
        // エラーが投げられた場合の処理
        print(error)
    }
}

エラーを返すことがない時にはthrowsは省略可能です

Task

Taskは、非同期作業の単位です。
非同期処理をキャンセル可能なものとしてグループ化し、管理する機能を持ちます。
Taskを使うと、新しい非同期処理を開始し、その処理をコード内の他の処理と並行して実行することができます。Task内で実行されるコードは、そのTaskが完了するまでの間、独立して実行されます。

またそれぞれのTaskはpriorityで呼び出す優先順位を変更できるようです。

// 非同期に何かのデータをフェッチするタスクを開始する
Task(priority: .high) {
    do {
        let result = try await fetchData()
        // 成功した場合の処理
    } catch {
        // エラーの場合の処理
    }
}

おわりに

今回はConcurrencyのメリットやキーワードについてざっくりと解説してみました。
今回取り扱ったものの他にも、データ競合を防ぐためのActor型や、非同期関数として反復処理を記述するAsyncSequenceなどConcurrencyを使いこなすためにはいろいろ覚えることが多いようです。
自分も勉強を続けて、使いこなせるようにしたいなと思いました。

【p.s】
個人的に勉強の教材にしているもののリンクを載せるので興味があったら見てみてください
https://techbookfest.org/product/4931999204638720?productVariantID=7KPLEF7ajX8v584xFbvBY5

4
2
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
4
2