Swift Structured Concurrency 面接質問: async/await、TaskGroup、Actors

Swift Structured Concurrency 技術面接質問: async/await、TaskGroup、actors と iOS 2026 向けの並行処理パターン

async/await、TaskGroup、actors を使用した Swift Structured Concurrency 面接質問

Swift 5.5 で導入された Structured Concurrency は、iOS の非同期プログラミングを革新しました。採用担当者は技術面接で async/await、TaskGroup、actors の習熟度を評価しています。面接で抜きん出るための重要な質問と期待される回答をご紹介します。

面接でテストされる主要スキル

採用担当者は3つのスキルを評価します: 基本概念の理解(async/await、Task)、並行処理パターンの習熟(TaskGroup、actor isolation)、よくあるエラー(data races、deadlocks)の診断能力です。

async/await と DispatchQueue の違いは何ですか?

期待される回答: async/await は読みやすい逐次的なコードで構造化された並行処理を提供しますが、DispatchQueue はコールバックを使用し、いわゆる「callback hell」を引き起こす可能性があります。Swift は async/await でスレッドを自動的に管理します。

NetworkService.swiftswift
// Comparison: async/await vs DispatchQueue

// ❌ Old style: DispatchQueue with callbacks
func fetchUserOld(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    DispatchQueue.global().async {
        // Simulated network call
        let result = self.performNetworkRequest(id: id)
        DispatchQueue.main.async {
            completion(result)
        }
    }
}

// ✅ New style: async/await more readable
func fetchUser(id: Int) async throws -> User {
    // Swift runtime handles threads automatically
    // No need to manually switch between queues
    return try await performNetworkRequest(id: id)
}

重要なポイント: async/await はコールバックのピラミッドを排除し、スレッドエラーを減らし(DispatchQueue.main.async は不要)、Swift ランタイムが利用可能な CPU コア間で実行を最適化できるようにします。

パフォーマンスの利点

Swift ランタイムは過剰なスレッド作成を回避する最適化されたスレッドプールを使用します。各 .async が新しいスレッドを作成する可能性のある DispatchQueue とは異なり、async/await は既存のスレッドをインテリジェントに再利用します。

複数の非同期処理を並列に管理するにはどうすればよいですか?

期待される回答: 単純な 2〜3 のタスクには async let を使用し、結果収集を伴う動的な数の並列タスクには TaskGroup を使用します。

DataFetcher.swiftswift
// Parallel async operations strategies

struct DataFetcher {
    // Strategy 1: async let for fixed tasks (2-4 operations)
    func loadDashboard() async throws -> Dashboard {
        // Launch 3 requests in parallel
        async let user = fetchUser()
        async let posts = fetchPosts()
        async let notifications = fetchNotifications()

        // Wait for results (parallel, not sequential)
        let (userData, postsData, notificationsData) = try await (user, posts, notifications)

        return Dashboard(user: userData, posts: postsData, notifications: notificationsData)
    }

    // Strategy 2: TaskGroup for dynamic number of tasks
    func downloadImages(urls: [URL]) async throws -> [UIImage] {
        // TaskGroup allows managing N tasks with result collection
        try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
            // Launch one task per URL
            for (index, url) in urls.enumerated() {
                group.addTask {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    guard let image = UIImage(data: data) else {
                        throw ImageError.invalidData
                    }
                    return (index, image) // Return index to preserve order
                }
            }

            // Collect results in order
            var images = [UIImage?](repeating: nil, count: urls.count)
            for try await (index, image) in group {
                images[index] = image
            }

            return images.compactMap { $0 }
        }
    }
}

よくある間違い: 呼び出しを並列化する async let の代わりに await を逐次的に使用することです。let user = await fetchUser(); let posts = await fetchPosts() は逐次的に実行されます(遅い)が、async let は両方を同時に起動します。

Actor とは何ですか、なぜ使用するのですか?

期待される回答: Actor は逐次アクセスを保証することでミュータブルな状態を data race から保護する型です。並行アクセスを保護するために手動のロック(NSLock、DispatchQueue)に取って代わります。

CacheManager.swiftswift
// Actor for thread-safe state management

// ❌ Classic class: data race risk
class UnsafeCache {
    private var cache: [String: Data] = [:] // Not thread-safe!

    func store(_ data: Data, for key: String) {
        cache[key] = data // ⚠️ Race condition with concurrent access
    }
}

// ✅ Actor: automatic protection against races
actor SafeCache {
    private var cache: [String: Data] = [:]

    // Sequential access guaranteed by actor isolation
    func store(_ data: Data, for key: String) {
        cache[key] = data // ✅ Thread-safe automatically
    }

    func retrieve(for key: String) -> Data? {
        return cache[key] // ✅ Protected read
    }

    // Internal synchronous method (no await needed)
    nonisolated func clearAll() async {
        // nonisolated allows calling from any context
        await self.clear()
    }

    private func clear() {
        cache.removeAll()
    }
}

// Usage: await required to access actor
let cache = SafeCache()
await cache.store(data, for: "user_123") // Await mandatory
let cachedData = await cache.retrieve(for: "user_123")

重要なポイント: Actor は一度に1つのスレッドのみがその状態にアクセスすることを保証します。コンパイラは外部呼び出しに対して await の使用を強制し、潜在的な中断ポイントを明示的にします。

MainActor の罠

@MainActor は UI 操作のためのグローバル actor です。クラスを @MainActor でマークすると、すべてのメソッドがメインスレッドで実行されることが強制されます。インターフェースをフリーズさせる可能性のあるブロッキング呼び出しに注意してください。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

TaskGroup でのエラーをどのように処理しますか?

期待される回答: withThrowingTaskGroup は最初に遭遇したエラーを伝播し、残りのタスクを自動的にキャンセルします。すべてのエラーを収集するには、TaskGroup 内で Result を使用します。

BatchProcessor.swiftswift
// Error handling strategies in TaskGroup

struct BatchProcessor {
    // Strategy 1: First error propagation (fail-fast)
    func processItemsFastFail(items: [Item]) async throws -> [ProcessedItem] {
        try await withThrowingTaskGroup(of: ProcessedItem.self) { group in
            for item in items {
                group.addTask {
                    // If one task throws, group cancels others
                    try await self.process(item)
                }
            }

            // Collect results until first error
            var results: [ProcessedItem] = []
            for try await result in group {
                results.append(result)
            }
            return results
        }
        // ⚠️ If one task fails, others are cancelled
    }

    // Strategy 2: Collect all errors (resilience)
    func processItemsResilient(items: [Item]) async -> ([ProcessedItem], [Error]) {
        await withTaskGroup(of: Result<ProcessedItem, Error>.self) { group in
            for item in items {
                group.addTask {
                    // Wrap in Result to capture errors
                    do {
                        let result = try await self.process(item)
                        return .success(result)
                    } catch {
                        return .failure(error)
                    }
                }
            }

            // Separate successes/failures
            var successes: [ProcessedItem] = []
            var errors: [Error] = []

            for await result in group {
                switch result {
                case .success(let item):
                    successes.append(item)
                case .failure(let error):
                    errors.append(error)
                }
            }

            return (successes, errors)
        }
    }

    private func process(_ item: Item) async throws -> ProcessedItem {
        // Processing with possible error
        try await Task.sleep(nanoseconds: 100_000_000)
        return ProcessedItem(from: item)
    }
}

重要なポイント: withThrowingTaskGroup は最初のエラーで停止します(アトミック操作に有用)が、withTaskGroup + Result はエラーがあっても続行できます(バッチ処理に有用)。

Task、Task.detached、async let の違いは何ですか?

期待される回答: Task は親のコンテキストを継承します(優先度、actor isolation)、Task.detached は継承なしの独立したタスクを作成し、async let はスコープの終わりに自動的に待機される子タスクを作成します。

TaskLifecycle.swiftswift
// Understanding Task creation patterns

@MainActor
class ViewModel {
    var isLoading = false

    // Scenario 1: Task inherits context (@MainActor here)
    func loadDataWithTask() {
        Task {
            // ✅ Inherits @MainActor from parent
            // No need for await MainActor.run
            self.isLoading = true
            let data = try await fetchData()
            self.isLoading = false // ✅ Always on MainActor
        }
    }

    // Scenario 2: Task.detached creates independent task
    func loadDataDetached() {
        Task.detached {
            // ⚠️ Does NOT inherit @MainActor
            let data = try await self.fetchData()

            // ❌ Error: isLoading not directly accessible
            // await MainActor.run {
            //     self.isLoading = false
            // }
        }
    }

    // Scenario 3: async let creates structured child task
    func loadMultipleData() async throws {
        // async let tasks are bound to current scope
        async let users = fetchUsers()
        async let posts = fetchPosts()

        // ⚠️ If leaving function before await, compilation error
        let (usersData, postsData) = try await (users, posts)

        // async let tasks automatically cancelled
        // if exiting scope (e.g., throw before await)
    }

    private func fetchData() async throws -> Data {
        try await URLSession.shared.data(from: URL(string: "https://api.example.com")!).0
    }

    private func fetchUsers() async throws -> [User] { [] }
    private func fetchPosts() async throws -> [Post] { [] }
}

ユースケース:

  • Task: 現在のコンテキストに紐づく操作(例: ViewModel からの UI 更新)
  • Task.detached: 独立したバックグラウンドタスク(例: ログ、analytics)
  • async let: 現在のスコープで結果が必要な並列操作

非同期操作にタイムアウトを実装するにはどうすればよいですか?

期待される回答: withThrowingTaskGroup でメインタスクとタイムアウトタスクの間のレースで Task.sleep を使用するか、withTimeout ユーティリティを作成します。

AsyncTimeout.swiftswift
// Timeout implementation for async operations

enum TimeoutError: Error {
    case timedOut
}

// Generic utility to add timeout
func withTimeout<T>(
    seconds: TimeInterval,
    operation: @escaping () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        // Task 1: main operation
        group.addTask {
            try await operation()
        }

        // Task 2: timeout
        group.addTask {
            try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
            throw TimeoutError.timedOut
        }

        // First task to finish wins
        guard let result = try await group.next() else {
            throw TimeoutError.timedOut
        }

        // Cancel losing task (important for cleanup)
        group.cancelAll()

        return result
    }
}

// Usage example
struct NetworkService {
    func fetchUserWithTimeout(id: Int) async throws -> User {
        // 5-second timeout on network call
        try await withTimeout(seconds: 5) {
            try await self.fetchUser(id: id)
        }
    }

    private func fetchUser(id: Int) async throws -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(User.self, from: data)
    }
}

現代的な代替案: iOS 16 以降では、HTTP 呼び出し専用に URLSessionConfiguration 経由で構成された timeoutInterval を使用した URLSession を使用します。

明示的なキャンセル

group.cancelAll() はリソースを解放するために重要です。これがないと、敗者のタスクは自然完了までバックグラウンドで実行を続け、CPU とメモリを浪費します。

複数のタスク間でミュータブルな状態を安全に共有するにはどうすればよいですか?

期待される回答: 共有状態には actor を使用するか、値のストリームを介してタスク間で通信するために AsyncStream を使用します。

SharedStateManager.swiftswift
// Safe state sharing between concurrent tasks

// Approach 1: Actor for shared state with sequential access
actor DownloadManager {
    private var activeDownloads: [String: Task<Data, Error>] = [:]
    private var cache: [String: Data] = [:]

    // Start download or return existing task
    func download(url: String) async throws -> Data {
        // Check cache first
        if let cachedData = cache[url] {
            return cachedData
        }

        // Check if download already in progress
        if let existingTask = activeDownloads[url] {
            return try await existingTask.value
        }

        // Create new download task
        let task = Task<Data, Error> {
            let data = try await self.performDownload(url: url)

            // Update cache (thread-safe via actor)
            await self.completeDownload(url: url, data: data)

            return data
        }

        activeDownloads[url] = task
        return try await task.value
    }

    private func performDownload(url: String) async throws -> Data {
        let urlObject = URL(string: url)!
        let (data, _) = try await URLSession.shared.data(from: urlObject)
        return data
    }

    private func completeDownload(url: String, data: Data) {
        cache[url] = data
        activeDownloads.removeValue(forKey: url)
    }
}

// Approach 2: AsyncStream for inter-task communication
struct EventStream {
    private let continuation: AsyncStream<Event>.Continuation
    let stream: AsyncStream<Event>

    init() {
        var continuation: AsyncStream<Event>.Continuation!
        stream = AsyncStream { cont in
            continuation = cont
        }
        self.continuation = continuation
    }

    func emit(_ event: Event) {
        continuation.yield(event)
    }

    func finish() {
        continuation.finish()
    }
}

// Example: shared progress monitoring
func processItemsWithProgress(items: [Item]) async {
    let eventStream = EventStream()

    // Task 1: Process items
    Task {
        for item in items {
            await processItem(item)
            eventStream.emit(.itemProcessed(item.id))
        }
        eventStream.finish()
    }

    // Task 2: Update UI with progress
    Task { @MainActor in
        for await event in eventStream.stream {
            switch event {
            case .itemProcessed(let id):
                print("Item \(id) processed")
            }
        }
    }
}

enum Event {
    case itemProcessed(String)
}

アーキテクチャの選択: ビジネスロジックを伴う集中状態には Actor、疎結合コンポーネント間のイベント駆動通信には AsyncStream を使用します。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Task のキャンセルとは何ですか、どのように処理しますか?

期待される回答: Task のキャンセルにより、進行中の非同期操作をキャンセルできます。タスクは定期的に Task.isCancelled を確認するか、エラーをスローする Task.checkCancellation() を使用する必要があります。

CancellableOperations.swiftswift
// Implementing proper task cancellation

struct ImageProcessor {
    // Cancellable processing with explicit checks
    func processImages(_ images: [UIImage]) async throws -> [ProcessedImage] {
        var results: [ProcessedImage] = []

        for (index, image) in images.enumerated() {
            // Check 1: Boolean check (continue or skip)
            if Task.isCancelled {
                print("Cancelled after \(index) images")
                break // Graceful stop
            }

            let processed = try await processImage(image)
            results.append(processed)

            // Check 2: Automatic throw if cancelled
            try Task.checkCancellation()
        }

        return results
    }

    private func processImage(_ image: UIImage) async throws -> ProcessedImage {
        // Simulate long processing
        for _ in 0..<10 {
            try await Task.sleep(nanoseconds: 100_000_000)

            // ✅ Check cancellation in long loops
            try Task.checkCancellation()
        }

        return ProcessedImage(from: image)
    }
}

// SwiftUI: Automatic cancellation when view disappears
struct ImageGalleryView: View {
    @State private var images: [ProcessedImage] = []

    var body: some View {
        ScrollView {
            // Display images
        }
        .task {
            // ✅ Task cancelled automatically when view disappears
            let processor = ImageProcessor()
            do {
                images = try await processor.processImages(sourceImages)
            } catch is CancellationError {
                print("Processing cancelled")
            }
        }
    }
}

// Manual cancellation of stored task
class DownloadViewModel {
    private var downloadTask: Task<Void, Never>?

    func startDownload() {
        downloadTask = Task {
            do {
                try await performLongDownload()
            } catch is CancellationError {
                print("Download cancelled by user")
            }
        }
    }

    func cancelDownload() {
        // Explicit cancellation of stored task
        downloadTask?.cancel()
        downloadTask = nil
    }

    private func performLongDownload() async throws {
        try Task.checkCancellation()
        // Download logic
    }
}

重要なポイント:

  • Task.isCancelled: 非ブロッキングチェック(bool を返す)
  • Task.checkCancellation(): キャンセルされた場合 CancellationError をスロー
  • SwiftUI モディファイア .task { }: ビューが消えると自動的にキャンセル
協調的キャンセル

Swift は協調的キャンセルモデルを使用します: タスクは強制的に終了されません。コードはキャンセルに反応するために Task.isCancelled または checkCancellation() を能動的にチェックする必要があります。これらのチェックなしでは、タスクは無期限に続行します。

SwiftUI アプリで MainActor を正しく使用するにはどうすればよいですか?

期待される回答: すべての UI 状態の更新がメインスレッドで行われることを保証するために、ViewModel に @MainActor のアノテーションを付けます。特定の操作のみが UI に触れる場合は、個々の関数に @MainActor を使用します。

MainActorPatterns.swiftswift
// Proper MainActor usage in SwiftUI architecture

// Pattern 1: Entire ViewModel @MainActor
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    // ✅ All methods implicitly @MainActor
    func loadUser(id: Int) async {
        isLoading = true // No need for await or MainActor.run
        errorMessage = nil

        do {
            // Network call done on background thread by runtime
            user = try await repository.fetchUser(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false // Always on MainActor
    }

    // Synchronous method also on MainActor
    func clearUser() {
        user = nil
        errorMessage = nil
    }
}

// Pattern 2: Selective methods with @MainActor
class DataSyncService {
    // ❌ Not @MainActor on class (no UI here)

    func syncData() async throws {
        // Background processing
        let data = try await fetchRemoteData()
        let processed = processData(data)

        // ✅ Switch to MainActor only for UI
        await updateUI(with: processed)
    }

    @MainActor
    private func updateUI(with data: ProcessedData) {
        // Update observable property
        NotificationCenter.default.post(
            name: .dataDidSync,
            object: data
        )
    }

    // Background work (not @MainActor)
    private func fetchRemoteData() async throws -> Data {
        // Network call
        Data()
    }

    private func processData(_ data: Data) -> ProcessedData {
        // CPU-intensive processing in background
        ProcessedData()
    }
}

// Pattern 3: Closure annotation
class ImageLoader {
    func loadImage(url: URL, completion: @MainActor @escaping (UIImage?) -> Void) async {
        let image = try? await downloadImage(from: url)

        // ✅ Completion guaranteed on MainActor
        await completion(image)
    }

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

よくある間違い: 特定のメソッドのみが UI に触れる場合に、クラス全体に @MainActor をマークすることです。これにより、バックグラウンドにあるべき重い操作を含むすべてのコードがメインスレッドで実行されます。

Sendable で data races をどのように処理しますか?

期待される回答: Sendable プロトコルは、型が data race のリスクなしにタスク間で共有できることを保証します。値型(struct、enum)は自動的に Sendable であり、クラスは不変または保護されたプロパティで final である必要があります。

SendableCompliance.swiftswift
// Making types safe for concurrent access

// ✅ Struct: automatically Sendable (value type)
struct UserData: Sendable {
    let id: Int
    let name: String
    let email: String
}

// ✅ Enum: automatically Sendable
enum LoadingState: Sendable {
    case idle
    case loading
    case loaded(UserData)
    case failed(Error) // ⚠️ Error must also be Sendable
}

// ❌ Class with mutable state: not Sendable by default
class UnsafeCounter {
    var count = 0 // Mutable, unprotected

    func increment() {
        count += 1 // Data race possible
    }
}

// ✅ Immutable class: explicit Sendable
final class SafeConfig: @unchecked Sendable {
    let apiKey: String
    let timeout: TimeInterval

    init(apiKey: String, timeout: TimeInterval) {
        self.apiKey = apiKey
        self.timeout = timeout
    }
}

// ✅ Class with actor-protected state
actor SafeCounter: Sendable {
    private var count = 0 // Protected by actor isolation

    func increment() {
        count += 1 // Thread-safe automatically
    }

    func getValue() -> Int {
        return count
    }
}

// ✅ Class with manually protected state
final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]

    func store(_ data: Data, for key: String) {
        lock.lock()
        defer { lock.unlock() }
        storage[key] = data
    }

    func retrieve(for key: String) -> Data? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }
}

// Usage: compiler checks Sendable
func processInBackground(data: UserData) { // ✅ UserData is Sendable
    Task.detached {
        // No warning: UserData is Sendable value type
        print("Processing user: \(data.name)")
    }
}

func processUnsafe(counter: UnsafeCounter) {
    Task.detached {
        // ⚠️ Warning: UnsafeCounter is not Sendable
        // counter.increment()
    }
}

Sendable のルール:

  • Sendable プロパティを持つ Struct/Enum: 自動的に Sendable
  • クラス: final + 不変であるか、手動の保護(lock、actor)と共に @unchecked Sendable を使用する必要があります
  • クロージャ: Sendable 型のみをキャプチャする場合、自動的に Sendable
@unchecked Sendable

@unchecked Sendable はコンパイラのチェックを無効にします。スレッド安全性が手動で保証されている場合(lock、シリアルキュー)にのみ使用します。data races を回避するのは開発者の責任です。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

まとめ

Swift Structured Concurrency の習得は、2026 年の iOS 面接にとって不可欠になりました。採用担当者は3つのレベルをテストします: 概念の理解(async/await vs callback)、パターンの習熟(TaskGroup、actor isolation)、デバッグ(キャンセル、Sendable)。

準備チェックリスト:

  • ✅ 具体例で async/await vs DispatchQueue を説明する
  • ✅ 並列操作のための TaskGroup の使用を実演する
  • ✅ ミュータブルな状態を保護するためのスレッドセーフな actor を実装する
  • ✅ 並行コンテキストでエラーを処理する(Result、throwing)
  • ✅ ユースケースで Task、Task.detached、async let を区別する
  • ✅ 非同期操作にタイムアウトを実装する
  • ✅ SwiftUI アーキテクチャで MainActor を正しく使用する
  • ✅ Sendable を理解し data races を回避する

トップ候補者は理論と実践を組み合わせます: 「なぜ」(data races の回避、可読性の向上)と「どのように」(エラー処理を伴う機能的なコード)を説明します。これらのパターンを定着させるために実プロジェクトで練習しましょう。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#swift
#concurrency
#async-await
#actors
#interview

共有

関連記事