Зачем нужна отдельная задача
Асинхронная функция сама по себе ничего не запускает — она просто умеет приостанавливаться. Чтобы код с await вообще начал выполняться, кому-то нужно его запустить. Представьте: у вас есть рецепт (асинхронная функция), но нужен ещё повар, который возьмётся его готовить прямо сейчас, параллельно с остальной кухней. Такого «повара» в Swift и создаёт Task.
Официально это называется unstructured task — задача, не привязанная жёстко к своему создателю: она стартует сразу после создания и живёт своей жизнью, пока не закончит работу или её не отменят.
Создание задачи
Простейший способ запустить асинхронный код — обернуть его в Task:
let task = Task {
try? await Task.sleep(for: .milliseconds(10))
return 21 * 2
}
Тело Task запускается почти сразу, ещё до того как вы что-то с ним сделаете. Чтобы дождаться результата, обратитесь к свойству value — оно приостановит текущий код до завершения задачи и вернёт то, что задача вернула:
let result = await task.value
print(result) // 42
Обращение к value не блокирует поток: пока задача работает, поток свободен и может обслуживать другой код, а когда результат готов — выполнение продолжается с этого места.
Приоритеты
У задачи можно указать приоритет — подсказку планировщику, насколько срочно её нужно выполнить относительно других задач:
let download = Task(priority: .background) {
try? await Task.sleep(for: .milliseconds(10))
return "готово"
}
Есть значения вроде .high, .medium, .low, .userInitiated, .background. Это именно подсказка: система старается выполнить более приоритетные задачи раньше, но не гарантирует точный порядок — если задач мало и ресурсов хватает, они всё равно выполнятся почти одновременно. Если приоритет не указать, задача унаследует его из окружающего контекста.
Отмена: кооперативная модель
У задачи можно вызвать cancel() — но это не «выключатель», который мгновенно обрывает выполнение:
task.cancel()
Этот вызов лишь помечает задачу отменённой. Сам код внутри Task должен periодически проверять, не отменили ли его, и сам решить, что делать — обычно завершиться пораньше. Это называется кооперативной отменой: задача сотрудничает с тем, кто её отменил, а не подчиняется силой.
Проверить отмену можно двумя способами. Внутри самой задачи — через статическое свойство Task.isCancelled:
let task = Task {
for step in 1...1000 {
if Task.isCancelled {
return nil
}
// здесь была бы часть полезной работы
}
return 1000
}
Второй способ — Task.checkCancellation(). Он ничего не возвращает, но выбрасывает ошибку CancellationError, если задачу отменили, — удобно в функциях, которые и так помечены throws:
func step() throws {
try Task.checkCancellation()
// дальше обычный код
}
Task.sleep — пауза без блокировки потока
Когда нужно смоделировать ожидание (запрос по сети, таймер, задержку), не пишите пустой цикл — используйте Task.sleep:
try? await Task.sleep(for: .milliseconds(10))
Task.sleep — это тоже точка приостановки: она умеет выбрасывать CancellationError, если задачу отменили во время сна, поэтому вызов часто оборачивают в try? — если сон прервался отменой, код просто идёт дальше, а последующая проверка isCancelled решает, что делать. Пока задача «спит», поток не простаивает впустую — он доступен для другой работы.
Частые ошибки
- Считать, что
cancel()сам останавливает код. Без проверкиisCancelledилиcheckCancellation()задача преспокойно доработает до конца. - Писать внутри задачи длинный цикл без единой точки приостановки и без проверки отмены — тогда её физически нельзя остановить, пока цикл не закончится сам.
- Забывать, что после отмены задача всё ещё может успеть вернуть обычное значение, если её тело не проверило
isCancelledперед возвратом, — «отменена» не значит «результата не будет». - Путать приоритет с гарантией порядка: высокий приоритет не означает, что задача выполнится строго раньше остальных.
Резюме
Task { ... }запускает асинхронный код немедленно, отдельно от вызывающего кода.await task.valueдожидается результата, не блокируя поток.priority— подсказка планировщику, а не жёсткая гарантия порядка.- Отмена кооперативная:
cancel()только ставит флаг, аTask.isCancelledиTask.checkCancellation()дают коду шанс среагировать. Task.sleep(for:)— способ поставить задачу на паузу, не занимая поток впустую.
Проверьте себя
4 вопроса
Отменяемое удвоение
Напишите функцию delayedDoubleValue(_:cancelImmediately:), которая запускает отдельную задачу Task, внутри неё делает короткую паузу через Task.sleep, а затем проверяет отмену.
Сигнатура: func delayedDoubleValue(_ n: Int, cancelImmediately: Bool) async -> Int?.
Требуемое поведение:
- Внутри функции создайте
let task = Task<Int?, Never> { ... }, тело которого сначала делаетtry? await Task.sleep(for: .milliseconds(10)), затем проверяетTask.isCancelled: если задача отменена — возвращаетnil, иначе возвращаетn * 2. - Если параметр
cancelImmediatelyравенtrue, вызовитеtask.cancel()сразу после создания задачи, до ожидания результата. - В конце верните
await task.value.
Пример: await delayedDoubleValue(5, cancelImmediately: false) должно вернуть 10, а await delayedDoubleValue(5, cancelImmediately: true) — nil.