Что такое обычный выход для группы задач в Swift Concurrency?

В сеансе WWDC Изучите структурированный параллелизм в Swift. Есть часть про нормальный выход группы задач.

Хотя группы задач представляют собой форму структурированного параллелизма, существует небольшая разница в том, как правило дерева задач реализуется для групповых задач по сравнению с асинхронными задачами. Предположим, при переборе результатов этой группы я столкнулся с дочерней задачей, которая завершилась с ошибкой. Поскольку эта ошибка выбрасывается из блока группы, все задачи в группе будут неявно отменены, а затем ожидаются. Это работает так же, как async-let. 

Разница возникает, когда ваша группа выходит за рамки обычного выхода из блока. Тогда отмена не является неявной. Такое поведение упрощает выражение шаблона разветвления-соединения с помощью группы задач, поскольку задания будут только ожидаться, а не отменяться. Вы также можете вручную отменить все задачи перед выходом из блока, используя метод cancelAll группы. Имейте в виду, что независимо от того, как вы отменяете задачу, отмена автоматически распространяется вниз по дереву.

Давайте воспользуемся примером.

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    var thumbnails: [String: UIImage] = [:]
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.async {
                return (id, try await fetchOneThumbnail(withID: id))
            }
        }
        // Obtain results from the child tasks, sequentially, 
        // in order of completion.
        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }
    }
    return thumbnails
}

Каков обычный выход в этом примере? Когда он достигнет return, группа задач выполнила все задачи, отменить нечего. Пожалуйста, помогите мне понять это.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
257
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Они просто говорят, что с async let, если вы пренебрегаете await этой задачей до того, как она выйдет из области видимости, она будет «неявно отменена». Но в случае с группой задач, если вы явно await не укажете отдельные групповые задачи, они не будут отменены неявно.

Когда в видео упоминается «нормальный выход», они просто говорят о «счастливом пути», где не произошло никаких ошибок, где ничего не было явно отменено и выполнение просто завершилось как обычно, без ошибок. О явной отмене и обработке ошибок они говорят в другом месте этого видео; они просто хотели привлечь наше внимание к тонкой разнице между тем, как ведет себя async let, если он выходит из области видимости без ожидания, и соответствующим поведением группы задач.


Рассмотрим этот пример в SE-0313:

func go() async { 
    async let f = fast() // 300ms
    async let s = slow() // 3seconds
    print("nevermind...")
    // implicitly: cancels f
    // implicitly: cancels s
    // implicitly: await f
    // implicitly: await s
}

По общему признанию, это надуманный пример, который вы, вероятно, никогда бы не сделали на практике. Более реалистичным примером может быть код, в котором после создания задач с помощью async let у нас может быть некоторая логика, использующая ранний выход, в результате чего она никогда не достигает await одной или нескольких задач async let. В этом сценарии с помощью async let любые задачи, которые не ожидаются явно, будут неявно отменены.

Обрисовав, что означает «неявная отмена», давайте теперь рассмотрим ваш пример:

func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
    try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
        for id in ids {
            group.addTask { [self] in
                try await (id, fetchOneThumbnail(withID: id))
            }
        }
        
        // Obtain results from the child tasks, sequentially,
        // in order of completion.

        var thumbnails: [String: UIImage] = [:]

        for try await (id, thumbnail) in group {
            thumbnails[id] = thumbnail
        }

        return thumbnails
    }
}

(Я заменил group.async на group.addTask и внес несколько косметических изменений, но по сути это то же самое, что и ваше.)

На самом деле это неприменимо к обсуждению «неявной отмены», потому что здесь есть цикл for, в котором есть await для каждой задачи в группе (по мере накопления результатов в словаре). Таким образом, сама идея «неявной отмены» неприменима, поскольку все дочерние задачи явно ожидаются.

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

func fetchThumbnails(for ids: [String]) async {
    await withTaskGroup(of: Void.self) { group in
        for id in ids {
            group.addTask { [self] in
                await fetchOneThumbnail(withID: id)
            }
        }

        // NB: no explicit `for await` loop of `group` sequence is needed; unlike 
        // `async let` pattern, these tasks will *not* be implicitly canceled
    }
}

Но в этом примере, хотя мы вообще никогда не for await последовательность group (таким образом, ни одна из дочерних задач не ожидается явно, в отличие от предыдущего примера), это не приведет к неявной отмене задач в группе, в отличие от async let. Он автоматически await выполнит все эти задачи за нас.

Короче говоря, async let неявно отменит все, что не ожидается явно, в то время как группа задач не будет неявно ничего отменять (по крайней мере, в сценарии «нормального выхода»), а скорее неявно await дочерние задачи.

Спасибо за подробное объяснение. Значит, пример без «ожидания» тоже верен? Он извлекает все миниатюры и обновляет внутренний кеш.

Alex 09.04.2024 02:49

Да, именно. Если задача на самом деле не имеет return ничего и вы используете with[Throwing][Discarding]TaskGroup(of: Void.self) {…}, то вам не нужно явно await указывать отдельные задачи. (Иногда мы можем, но часто в этом нет необходимости.) Цель этих цитат в вашем вопросе заключалась в том, чтобы просто подчеркнуть, что хотя async let и группы задач делают что-то очень похожее (параллельно запускают различные дочерние задачи), это одно из отличий. между двумя шаблонами, а именно, что группы задач не отменяются неявно, если вы не ожидаете отдельные дочерние задачи.

Rob 09.04.2024 03:29

func go() async { async let f = fast() // 300 мс async let s = медленный() // 3 секунды print("неважно...") // неявно: отменяет f // неявно: отменяет s // неявно : await f // неявно: await s } Насколько я понимаю, когда эта функция выходит за пределы области видимости, дочерние задачи немедленно отменяются. Что ждет после отмены в комментариях?

Alex 09.04.2024 17:54

Отмена является «совместной» (это означает, что бремя фактической отмены зависит от того, как эта задача была реализована; отмена может быть не немедленной, а может вообще не поддерживать отмену). Поэтому он запрашивает отмену задачи, но ожидает успешной отмены или, если задача не поддерживает отмену, ожидает ее завершения.

Rob 09.04.2024 18:25

Хорошо написанный асинхронный код всегда должен поддерживать достаточно быструю отмену (и это делает большинство асинхронных API Apple), но обработка отмены вручную (например, esp в наших собственных вычислительных задачах) может варьироваться. Например, при обновлении отдельных пикселей изображения я могу проверять отмену только один раз для каждой строки или что-то еще, чтобы обеспечить хороший баланс между поддержкой отмены и снижением накладных расходов. Иногда мы вообще не можем поддерживать отмену при вызове какого-либо синхронного API (например, при записи какого-то огромного ресурса в файл)

Rob 09.04.2024 18:33

Другие вопросы по теме