Task Group в Swift объясняются примерами кода

Task group в Swift позволяют объединять несколько параллельных задач и ждать возврата результата, когда все задачи будут завершены. Они обычно используются для таких задач, как объединение нескольких ответов на запросы API в один объект ответа.

Сначала прочитайте мою статью о задачах, если вы новичок в них, и убедитесь, что вы прочитали мою статью о async/await, поскольку сегодня это основа Task group. Имея это в виду, мы готовы перейти к деталям.

Что такое Task group?

Вы можете видеть группу задач как контейнер нескольких дочерних задач, которые добавляются динамически. Дочерние задачи могут выполняться параллельно или последовательно, но группа задач будет помечена как завершенная только после выполнения ее дочерних задач.

Типичным примером может быть загрузка нескольких изображений из фотогалереи:

await withTaskGroup(of: UIImage.self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }
}

Сначала мы загружаем список URL-адресов фотографий галереи в приведенном выше примере. Обратите внимание, что эта задача не связана с нашей группой и тем самым не влияет на состояние группы задач. Во-вторых, мы перебираем URL каждой фотографии и начинаем загружать их параллельно. Метод withTaskGroup вернется после загрузки всех фотографий.

Как использовать Task group

Вы можете группировать задачи несколькими способами, включая обработку ошибок или возврат окончательного набора результатов. Они являются более продвинутой альтернативой sync let и позволяют динамически добавлять задачи.

Возврат финальной коллекции результатов

Первый пример, представленный в этой статье, касался объединения задач без возврата окончательной коллекции изображений. Мы могли бы переписать этот пример и вместо этого вернуть коллекцию изображений:

let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }

    var images = [UIImage]()
    for await result in taskGroup {
        images.append(result)
    }
    return images
}

Мы определили возвращаемый тип как набор изображений, используя [UIImage].self. После запуска всех дочерних задач мы используем async sequence, чтобы дождаться следующего результата и добавить изображение результата в нашу коллекцию результатов.

Группы задач соответствуют AsyncSequence, что позволяет нам переписать приведенный выше код с помощью оператора сокращения:

let images = await withTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { await downloadPhoto(url: photoURL) }
    }

    return await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

Также можно использовать другие операции, такие как map и flatMap, что позволяет использовать гибкие решения для создания результата. Наконец, вы можете использовать коллекцию изображений и продолжить рабочий процесс.

Обработка ошибок с помощью throwing варианта

Обычно методы загрузки изображений выдают ошибку при сбое. Мы можем переписать наш пример для обработки этих случаев, переименовав withTaskGroup в withThrowingTaskGroup:

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }

    return try await taskGroup.reduce(into: [UIImage]()) { partialResult, name in
        partialResult.append(name)
    }
}

Обратите внимание, что мы добавили ключевое слово try перед каждым асинхронным методом, поскольку нам приходится обрабатывать потенциально возникающие ошибки. Результатом будет ThrowingTaskGroup, который будет возвращать результат изображений до тех пор, пока не возникнет ошибка.

НЕУДАЧА ГРУППЫ, КОГДА ДЕТСКАЯ ЗАДАЧА ВЫДАЕТ ОШИБКУ

В приведенном выше примере наша группа не потерпит неудачу, если дочерняя задача загрузки выдаст ошибку. Чтобы это произошло, нам нужно изменить то, как мы перебираем результаты, используя метод next():

let images = try await withThrowingTaskGroup(of: UIImage.self, returning: [UIImage].self) { taskGroup in
    let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
    for photoURL in photoURLs {
        taskGroup.addTask { try await downloadPhoto(url: photoURL) }
    }

    var images = [UIImage]()

    /// Note the use of `next()`:
    while let downloadImage = try await taskGroup.next() {
        images.append(downloadImage)
    }
    return images
}

Метод next() получает ошибки от отдельных задач, что позволяет вам обрабатывать их соответствующим образом. В этом случае мы перенаправляем ошибку на закрытие группы, что приводит к сбою всей группы задач. Любые другие запущенные дочерние задачи будут отменены в этот момент.

Избегайте одновременных мутаций

Вы должны понимать, что не должны изменять группу задач вне задачи, в которой вы ее создали. Например, не передавайте его и не добавляйте к нему дочерние задачи из другой задачи. В большинстве случаев вы должны быть предупреждены системой типов Swift, когда вы это делаете, поскольку такие операции мутации не могут выполняться из контекста параллельного выполнения, такого как дочерняя задача.

Отмены в группах

Вы можете отменить группу задач, отменив задачу, в которой она выполняется, или вызвав метод cancelAll() для самой группы.

Когда задачи добавляются в отмененную группу с помощью addTask(), они будут отменены сразу после создания. Он остановит свою работу напрямую в зависимости от того, правильно ли эта задача соблюдает отмену. При желании вы можете использовать addTaskUnlessCancelled(), чтобы предотвратить запуск задачи.

Создание построителя результатов группы задач

Запомнить, как объединять задачи в группу с помощью предыдущих общих примеров кода, может быть непросто. Вы должны вызвать метод addTask(), дождаться результатов и добавить их в коллекцию, прежде чем вы получите конечный результат.

Вместо этого мы можем создать собственный построитель результатов, что позволит нам переписать приведенный выше код следующим образом:

let photoURLs = try await listPhotoURLs(inGallery: "Amsterdam Holiday")
let images = try await withThrowingTaskGroup {
    for photoURL in photoURLs {
        Task { try await downloadPhoto(url: photoURL) }
    }
}

В результате получается компактный, более читаемый код, в то время как мы все еще выполняем задачи параллельно. Вы можете изучить код для приведенного выше построителя результатов в репозитории GitHub.