$ sudo teach IT

Зачем нужна отдельная задача

Асинхронная функция сама по себе ничего не запускает — она просто умеет приостанавливаться. Чтобы код с 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.