Перейти к содержимому
Теория · Асинхронный код · урок 3 из 7

Отдельная задача: Task

Кассир в кафе с пейджерами не может отойти к столику и ждать сигнала: за ним очередь. А дела с ожиданием у него бывают. Например, кончилось молоко: нужно заказать его у поставщика и дождаться доставки.

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

С обычной функцией в программе то же самое: await в ней писать нельзя, а ждать иногда нужно и оттуда. Например, в приложении на нажатие кнопки отвечает обычная функция, а ей нужно загрузить данные с сервера.

Задачи и Task

Для этого есть Task. Задача — работа, которую Swift выполняет отдельно от остального кода, как поручение на листке. Task { … } создаёт задачу, и внутри её фигурных скобок await разрешён, даже если вокруг обычный код.

С типом Task вы уже встречались: Task.sleep — его метод типа. А запись Task { … } вызывает инициализатор Task и передаёт ему замыкание с работой. Других аргументов нет, поэтому замыкание стоит сразу после имени типа, а круглые скобки опущены.

SWIFT
let task = Task {
    print("inside") // inside
}
await task.value
print("end") // end

Разберём по строкам:

  1. Task { … } создаёт задачу и сразу возвращает её. Код в скобках выполняется отдельно, а строки после Task идут дальше, не дожидаясь его.
  2. В константе task теперь лежит сама задача.
  3. await task.value ждёт, пока задача закончится. За это время задача выполняется и печатает inside.
  4. print("end") печатает end.

Когда задача выполняется

Задача, запущенная в main.swift, выполняется в той же главной части программы, что и строки файла. Как листок на штыре, она получает очередь, только когда строки файла остановятся на await. В примере это await task.value: строки ждут, а в освободившееся время задача печатает inside.

В примере задача стоит прямо в main.swift, где await и так разрешён: так проще проследить, в каком порядке идут строки. В обычной функции Task { … } пишут так же.

Здесь картинка с листком неточна. Листок ждёт, пока у кассира выдастся свободная минута. А задачу, запущенную внутри функции, Swift может выполнить одновременно с остальным кодом, не дожидаясь никакого await.

Результат задачи

У задачи бывает результат, и value — это он. У задачи из первого примера результата нет, поэтому await task.value там нужен ради ожидания. А эта задача готовит кофе и возвращает строку:

SWIFT
func makeCoffee() async throws -> String {
    try await Task.sleep(for: .milliseconds(100))
    return "coffee"
}

let order = Task {
    try await makeCoffee()
}
print("order") // order
let drink = try await order.value
print(drink) // coffee

Разберём, что происходит:

  1. Тело замыкания — одно выражение, поэтому задача возвращает строку от makeCoffee без return.
  2. Задача создана и ждёт своей очереди, а print("order") печатает order.
  3. try await order.value ждёт конца задачи и отдаёт её результат. makeCoffee может выбросить ошибку, и value передаст её дальше, поэтому нужен try.
  4. В drink попадает строка "coffee", и print(drink) печатает coffee.

Частая ошибка

Задачу запустили, но не дождались:

SWIFT
Task { // ✗
    print("inside")
}
print("end")

Программа печатает только end, и так при каждом запуске. Строки файла ни разу не останавливаются на await, поэтому задача не получает очереди. После print("end") программа завершается, не дожидаясь задач, и до кода в скобках дело так и не доходит. Кафе закрылось, а листок остался на штыре.

В приложении, которое работает дальше, задача выполнится, а здесь её нужно дождаться через await task.value, как в примере выше.

Главное

  • Task { … } создаёт задачу — работу, которую Swift выполняет отдельно от остального кода; внутри её скобок await разрешён.
  • Задача в main.swift получает очередь, только когда строки файла остановятся на await; сама программа задачу не ждёт.
  • await task.value ждёт конца задачи и отдаёт её результат; если код задачи может выбросить ошибку, пишут try await.