SwiftSeniorTechnical

Что такое Task в Swift Concurrency и что такое TaskGroup?

Task запускает одну асинхронную задачу с возможностью отмены через task.cancel() и проверкой Task.isCancelled. TaskGroup (withTaskGroup/withThrowingTaskGroup) запускает N параллельных задач и собирает результаты через for await in group.

Task и TaskGroup в Swift Concurrency

Task и TaskGroup — основные примитивы структурированного параллелизма в Swift, введённые в Swift 5.5 (iOS 15+).

Task — одиночная асинхронная задача

// Неструктурированная Task — запускается немедленно
let task = Task {
    let result = await fetchData()
    print(result)
}

// Ожидание результата
let value = await task.value

// Отмена
task.cancel()

Типы Task

// Task с возвратом значения
let task = Task<String, Error> {
    try await fetchString()
}
let result = try await task.value

// Task с приоритетом
let backgroundTask = Task(priority: .background) {
    await processLargeDataset()
}

// Task.detached — не наследует actor context и приоритет
let detached = Task.detached(priority: .low) {
    // НЕ наследует @MainActor даже если вызван с главного актора
    await backgroundWork()
}

Отмена задач

@MainActor
class SearchViewModel: ObservableObject {
    @Published var results: [String] = []
    private var searchTask: Task<Void, Never>?

    func search(query: String) {
        searchTask?.cancel() // отменяем предыдущий поиск
        searchTask = Task {
            do {
                try Task.checkCancellation() // ранняя проверка
                let data = try await searchAPI(query: query)
                try Task.checkCancellation() // после долгой операции
                self.results = data
            } catch is CancellationError {
                // нормальная отмена, не логируем
            } catch {
                print("Search failed:", error)
            }
        }
    }

    deinit { searchTask?.cancel() }
}

TaskGroup — параллельные задачи

TaskGroup позволяет запускать несколько задач параллельно и собирать результаты.

// withTaskGroup — без throws
func fetchAllUsers(ids: [UUID]) async -> [User] {
    await withTaskGroup(of: User?.self) { group in
        for id in ids {
            group.addTask {
                try? await fetchUser(id: id) // ошибки игнорируются
            }
        }
        var users: [User] = []
        for await user in group {
            if let user { users.append(user) }
        }
        return users
    }
}

// withThrowingTaskGroup — с propagation ошибок
func fetchRequiredUsers(ids: [UUID]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await fetchUser(id: id) }
        }
        var users: [User] = []
        for try await user in group { // первая ошибка отменяет группу
            users.append(user)
        }
        return users
    }
}

Ограничение параллелизма

// Максимум N параллельных задач
func processWithLimit<T>(_ items: [T], maxConcurrency: Int, work: @escaping (T) async throws -> Void) async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        var activeTasks = 0

        for item in items {
            if activeTasks >= maxConcurrency {
                try await group.next() // ждём завершения одной задачи
                activeTasks -= 1
            }
            group.addTask { try await work(item) }
            activeTasks += 1
        }
        try await group.waitForAll()
    }
}

// Использование: не более 3 параллельных загрузок
try await processWithLimit(urls, maxConcurrency: 3) { url in
    await downloadImage(from: url)
}

Подводные камни

  • Task.detached не наследует actor context — вызов UI-обновлений из detached task без await MainActor.run вызывает ошибки в Swift 6.
  • Отмена TaskGroup отменяет все дочерние задачи, но не ждёт их завершения — результаты уже начатых задач теряются.
  • Task.checkCancellation() бросает CancellationError — не забывайте обрабатывать его отдельно от других ошибок.
  • Создание тысяч Task {} без ограничения параллелизма исчерпывает thread pool — используйте TaskGroup с ограничением.
  • В withThrowingTaskGroup первая ошибка из for try await отменяет остальные задачи — если нужны все результаты, собирайте ошибки в Result.
  • Task не сохраняет приоритет при передаче через actor boundary — используйте явный priority:.
  • iOS 15 minimum target для async/await — на iOS 13-14 нужен withCheckedContinuation для обёртки completion-based API.
  • ViewModel с @MainActor и TaskGroup: дочерние задачи в группе не наследуют MainActor — UI обновления нужно явно переносить через await MainActor.run.

Common mistakes

  • Сводить «Task в Swift Concurrency и что такое TaskGroup» к синтаксису и не объяснять ARC.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-16.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «Task в Swift Concurrency и что такое TaskGroup» и подтверждает ее корректным примером.
  • Умеет связать ответ с Swift Concurrency, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swift-16, включая производительность, память и сопровождение.

Sources

Related topics