Отдельные задачи в Swift объясняются примерами кода

Отсоединенные задачи позволяют создать новую задачу верхнего уровня и отключиться от текущего структурированного контекста параллелизма. Вы можете возразить, что их использование приводит к неструктурированному параллелизму, поскольку вы отключаете потенциально важные задачи.

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

Что такое отдельная задача?

Отсоединенная задача выполняет данную операцию асинхронно как часть новой задачи верхнего уровня.

Task.detached(priority: .background) {
    // Runs asynchronously
}

Код внутри замыкания будет выполняться асинхронно из родительского контекста.

Следующий код демонстрирует эту концепцию, используя асинхронный метод для вывода значения:

await asyncPrint("Operation one")
Task.detached(priority: .background) {
    // Runs asynchronously
    await self.asyncPrint("Operation two")
}
await asyncPrint("Operation three")

func asyncPrint(_ string: String) async {
    print(string)
}

// Prints:
// Operation one
// Operation three
// Operation two

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

Риски использования отдельных задач

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

let outerTask = Task {
    /// This one will cancel.
    await longRunningAsyncOperation()

    /// This detached task won't cancel.
    Task.detached(priority: .background) {
        /// And, therefore, this task won't cancel either.
        await self.longRunningAsyncOperation()
    }
}
outerTask.cancel()

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

Наконец, поскольку вы выполняете код асинхронно, вам придется использовать «я», чтобы явно указать семантику захвата:

Еще одним признаком риска, связанного с отключением от родительского локального хранилища, является то, что вы с большей вероятностью столкнетесь с циклами сохранения.

Когда использовать отсоединенную задачу

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

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

Task.detached(priority: .background) {
    await DirectoryCleaner.cleanup()
}

В этом примере мы не ссылаемся ни на какие локальные ссылки, используя self. Код очистки запускается независимо, может продолжаться, пока родительский контекст отменяется, и выполняется с использованием фонового приоритета.