Задачи в Swift объясняются примерами кода

Задачи в Swift являются частью среды параллелизма, представленной на WWDC 2021. Задача позволяет нам создавать параллельную среду из непараллельного метода, вызывая методы с помощью async/await.

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

Если вы новичок в async/await, я рекомендую сначала прочитать мою статью Async await в Swift с примерами кода.

Как создать и запустить задачу

Создание базовой задачи в 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

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

Как запустить задачу?

Ну, приведенные выше примеры уже дали ответ для этого раздела. Задача запускается сразу после создания и не требует явного запуска. Важно понимать, что задание выполняется сразу после создания, поскольку оно говорит вам создавать его только тогда, когда его работа разрешена.

Выполнение асинхронных методов внутри задачи

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

«асинхронный» вызов функции, которая не поддерживает параллелизм

В этом примере метод executeTask представляет собой простую оболочку для другой задачи:

func executeTask() async {
    let basicTask = Task {
        return "This is the result of the task"
    }
    print(await basicTask.value)
}

Мы можем решить вышеуказанную ошибку, вызвав метод executeTask() в новой задаче:

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(). Интересно, что наш код выполняется, несмотря на то, что мы не сохранили ссылку на созданную задачу в методе появления, что привело нас к следующему разделу: отмена.

Отмена обработки

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

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

Отмена задачи

Чтобы объяснить вам, как работает отмена, мы будем работать с новым примером кода, который загружает изображение:

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

Вы можете установить точку останова, чтобы проверить, в каком потоке работает ваш метод: