Task в Swift являются частью среды параллелизма, представленной на WWDC 2021. Task позволяет нам создавать параллельную среду из непараллельного метода, вызывая методы с помощью async/await.
При работе с Task в первый раз вы можете распознать сходство между очередями отправки и задачами. Оба позволяют распределять работу по другому потоку с определенным приоритетом. Тем не менее, Task совсем другие и облегчают нашу жизнь, убирая многословность очередей отправки.
Если вы новичок в async/await, я рекомендую сначала прочитать мою статью Async await в Swift с примерами кода.
Как создать и запустить Task
Создание базовой Task в Swift выглядит следующим образом:
let basicTask = Task {
return "This is the result of the task"
}
Как видите, мы сохраняем ссылку на нашу basicTask, которая возвращает строковое значение. Мы можем использовать ссылку, чтобы прочитать значение результата:
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
// Prints: This is the result of the task
Этот пример возвращает строку, но также мог вызывать ошибку:
let basicTask = Task {
// .. perform some work
throw ExampleError.somethingIsWrong
}
do {
print(try await basicTask.value)
} catch {
print("Basic task failed with error: \(error)")
}
// Prints: Basic task failed with error: somethingIsWrong
Другими словами, вы можете использовать Task для получения как значения, так и ошибки.
Как запустить Task?
Ну, приведенные выше примеры уже дали ответ для этого раздела. Task запускается сразу после создания и не требует явного запуска. Важно понимать, что задание выполняется сразу после создания, поскольку оно говорит вам создавать его только тогда, когда его работа разрешена.
Выполнение async методов внутри задачи
Помимо синхронного возврата значения или выдачи ошибки, Task также выполняют асинхронные методы. Нам нужна Task для выполнения любых асинхронных методов внутри функции, которая не поддерживает параллелизм. Следующая ошибка может быть вам уже знакома:
‘async’ call in a function that does not support concurrency («асинхронный» вызов функции, которая не поддерживает параллелизм)
В этом примере метод executeTask представляет собой простую оболочку для другой задачи:
func executeTask() async {
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
}
Мы можем решить вышеуказанную ошибку, вызвав метод executeTask() в новой Task:
var body: some View {
Text("Hello, world!")
.padding()
.onAppear {
Task {
await executeTask()
}
}
}
func executeTask() async {
let basicTask = Task {
return "This is the result of the task"
}
print(await basicTask.value)
}
Задача создает среду поддержки параллелизма, в которой мы можем вызывать асинхронный метод executeTask(). Интересно, что наш код выполняется, несмотря на то, что мы не сохранили ссылку на созданную Task в методе появления, что привело нас к следующему разделу: отмена.
Отмена обработки
Глядя на отмену, вы можете быть удивлены, увидев, что ваша задача выполняется, даже если вы не сохранили ссылку на нее. Подписки издателя в Combine требуют, чтобы мы поддерживали сильную ссылку, чтобы гарантировать передачу значений. По сравнению с Combine вы можете ожидать, что Task также будет отменена после освобождения всех ссылок.
Однако Tasks работают по-разному, поскольку они выполняются независимо от того, сохраняете ли вы ссылку. Единственная причина сохранить ссылку — дать себе возможность дождаться результата или отменить задачу.
Отмена задачи
Чтобы объяснить вам, как работает отмена, мы будем работать с новым примером кода, который загружает изображение:
struct ContentView: View {
@State var image: UIImage?
var body: some View {
VStack {
if let image = image {
Image(uiImage: image)
} else {
Text("Loading...")
}
}.onAppear {
Task {
do {
image = try await fetchImage()
} catch {
print("Image loading failed: \(error)")
}
}
}
}
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
return try await imageTask.value
}
}
Приведенный выше пример кода извлекает случайное изображение и отображает его соответствующим образом, если запрос выполнен успешно.
Ради этой демонстрации мы могли бы отменить imageTask сразу после его создания:
func fetchImage() async throws -> UIImage? {
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
return try await imageTask.value
}
Приведенного выше вызова отмены достаточно, чтобы остановить выполнение запроса, поскольку реализация URLSession выполняет проверки отмены перед выполнением. Таким образом, приведенный выше пример кода выводит следующее:
Starting network request...
Image loading failed: Error Domain=NSURLErrorDomain Code=-999 "cancelled"
Как видите, наш оператор печати все еще выполняется. Этот оператор печати — отличный способ продемонстрировать, как реализовать проверки отмены с помощью одного из двух методов статической проверки отмены. Первый останавливает выполнение текущей задачи, выдавая ошибку при обнаружении отмены:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
/// Throw an error if the task was already cancelled.
try Task.checkCancellation()
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
Вышеупомянутые результаты кода печатают:
Image loading failed: CancellationError()
Как видите, ни наш оператор печати, ни сетевые запросы не вызываются.
Второй метод, который мы можем использовать, дает нам логический статус отмены. Используя этот метод, мы позволяем себе выполнять любые дополнительные очистки при отмене:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
guard Task.isCancelled == false else {
// Perform clean up
print("Image request was cancelled")
return nil
}
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
return UIImage(data: imageData)
}
// Cancel the image request right away:
imageTask.cancel()
В этом случае наш код выводит только заявление об отмене.
Выполнение регулярных проверок отмены необходимо, чтобы ваш код не выполнял ненужную работу. Представьте себе пример, в котором мы преобразовали бы возвращенное изображение; возможно, нам стоило добавить несколько проверок по всему коду:
let imageTask = Task { () -> UIImage? in
let imageURL = URL(string: "https://source.unsplash.com/random")!
// Check for cancellation before the network request.
try Task.checkCancellation()
print("Starting network request...")
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
// Check for cancellation after the network request
// to prevent starting our heavy image operations.
try Task.checkCancellation()
let image = UIImage(data: imageData)
// Perform image operations since the task is not cancelled.
return image
}
Мы контролируем отмену, что позволяет легко совершать ошибки и выполнять ненужную работу. Будьте внимательны при реализации задач, чтобы ваш код регулярно проверял состояние отмены.
Установка приоритета
Каждая задача может иметь свой приоритет. Значения, которые мы можем применить, аналогичны уровням качества обслуживания, которые мы можем настроить при использовании очередей отправки. Низкий, средний и высокий приоритеты похожи на приоритеты, установленные с помощью операций.
Каждый приоритет имеет свою цель и может указывать на то, что работа важнее других. Нет никакой гарантии, что ваша задача действительно выполнится раньше. Например, может уже выполняться задание с более низким приоритетом.
Настройка приоритета помогает не допустить, чтобы задача с низким приоритетом избегала выполнения задачи с более высоким приоритетом.
Поток, используемый для выполнения
По умолчанию задача выполняется в автоматически управляемом фоновом потоке. В ходе тестирования я обнаружил, что приоритет по умолчанию равен 25. Распечатка необработанного значения высокого приоритета показывает совпадение:
(lldb) p Task.currentPriority
(TaskPriority) $R0 = (rawValue = 25)
(lldb) po TaskPriority.high.rawValue
25
Вы можете установить точку останова, чтобы проверить, в каком потоке работает ваш метод:
Продолжаем свое путешествие в Swift Concurrency
Изменения параллелизма — это больше, чем просто async-await, они включают в себя множество новых функций, которые вы можете использовать в своем коде. Теперь, когда вы узнали об основах задач, пришло время погрузиться в другие новые функции параллелизма:
- Tasks in Swift explained with code examples
- Async await in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- MainActor usage in Swift explained to dispatch to the main thread
- Actors in Swift: how to use and prevent data races
Заключение
Задачи в Swift позволяют нам создать параллельную среду для запуска async методов. Отмена требует явных проверок, чтобы убедиться, что мы не выполняем ненужную работу. Настроив приоритет наших задач, мы можем управлять порядком выполнения.
Если вы хотите узнать больше советов по Swift, посетите страницу категории Swift. Если у вас есть какие-либо дополнительные предложения или отзывы, не стесняйтесь обращаться ко мне или писать мне в Твиттере.
Спасибо!