XCode concurrency with Async/Await

Глава 5. Параллельное программирование в Swift

Оглавление

Введение

Параллельное программирование с использованием конструкций Async/Await

Параллельное программирование в Swift при работе с UI

Заключение

Введение

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

На сегодняшний день соперничество в быстродействии процессоров, используемых в моделях IPhone и IPad и процессоров,  устанавливаемых в настольных ПК или ноутбуках , все так же представляет проблему в выполнении программ последовательно, какими бы мощными и быстрыми не были новые линейки процессоров A(A14, A15 и т.д.). Несмотря на то, что у нас самый современный процессор в телефоне, наше приложение при последовательном выполнении программы будет все так же покорно ждать пока мы выполняем запрос с интернет сервера в получении каких то данных или вообще в ожидании загрузки данных на сервер. И только по окончании выполнении задачи приема данных, наше приложение продолжит работу, как показано на рисунке 5.1:

Concurrency development Swift

Рис 5.1 Последовательное выполнение задач приводящее к блокированию работы приложения

Частично мы эту задачу уже рассматривали в Главе 4, посвященной Grand Central Dispatch. Так как долго выполняющиеся задачи, такие как передача данных по сети, или ожидание включения Bluetooth, могут блокировать основной поток приложения, лучшим решением будет запуск 2 или более задач одновременно. Это означает то, что пока одна задача ожидает своего завершения, остальные задачи будут работать в своем обычном режиме. Таким образом, программа может взаимодействовать с пользователем в обычном режиме, не важно как долго будут выполняться задачи, запущенные параллельно. Этот принцип хорошо показан на рисунке 5.2

Sequential processing data

Рис. 5.2 Параллельное одновременное выполнение 2-х или более задач

Параллельное программирование с использованием конструкций Async/Await

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

В первых версиях языка Swift, вызовов асинхронного обработчика функции выглядит следующим образом:

func doSomething(completion:(Result<Response>, BigError) -> Void){
    //task
}

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

  1. Создадим новый iOs проект Chapter5. Обязательно проверьте, чтобы тип интерфейса стоял SwiftUI.
  2. Создадим новый Playground файл и назовем его CompletionHandler
  3. Добавим следующий код:
    import UIKit
    
    enum BigError: Error{
        case powerOutage
        case endOfTheWorld
    }
    
    enum Response{
        case success
        case failure
    }
    
    let startTime = NSDate()
    func doSomething(completion: (Result<Response, BigError>) -> Void){
        print("Starting task")
        Thread.sleep(forTimeInterval: 2)
        
        let randomNumber = Int.random(in: 0..<2)
        
        if randomNumber == 0{
            completion(.failure(.powerOutage))
            return
        }
        completion(.success(.success))
    }
    
    //Calling the function
    doSomething{ result in
        switch result{
        case .success(let response):
            print("Result = \(response)")
        case .failure(let error):
            print("This is the error = \(error)")
        }
        print("Ending task")
    }
    
    let endTime = NSDate()
    print("Completed in \(endTime.timeIntervalSince(startTime as Date)) seconds")
    
  4. Запустим наш маленький проект на выполнение, нажав на кнопку Run

XCode concurrency with Async/Await

Обработчик завершения задачи часто называются неструктурированным параллельным обработчиком, потому что они тяжелы для чтения и понимания. Обратим внимание, как функция включает в себя 2 варианта для вызова обработчика завершения задачи:

func doSomething(completion: (Result<Response, BigError>) -> Void){
    print("Starting task")
    Thread.sleep(forTimeInterval: 2)
    
    let randomNumber = Int.random(in: 0..<2)
    
    if randomNumber == 0{
        completion(.failure(.powerOutage))
        return
    }
    completion(.success(.success))
}

Если мы не предусмотрим все варианты окончания работы программы, к сожалению наша функция будет выполняться бесконечно, наглухо заморозив приложение. Для того, чтобы параллельное программирование сделать более простым к чтению и пониманию, в языке Swift реализовали языковую конструкцию async/await. Ключевое слово async определяет асинхронную функцию. Для вызова этой асинхронной функции мы используем ключевое слово await. Также о параллельном программировании и асинхронных функциях Вы можете прочитать в Главе 4.

Давайте перейдем к практической части рассмотрения вопроса работы связки async/await в Swift.

  1. Создадим новый iOs Playground файл и назовем его AsyncAwait. Напоминаем, что все примеры Вы можете найти и скачать в нашем GitHub’e.
  2. Добавим следующий код:
    import UIKit
    
    enum BigError: Error{
        case powerOutage
        case endOfTheWorld
    }
    
    enum Response{
        case success
        case failure
    }
    
    let startTime = NSDate()
    func doSomething() async throws -> Response{
        print("Starting task")
        Thread.sleep(forTimeInterval: 2)
        
        let randomNumber = Int.random(in: 0..<2)
        
        if randomNumber == 0{
            throw BigError.powerOutage
            
        }
        return Response.success
    }
    
    //Calling the function
    func callFunction(){
        Task(priority: .low){
            do{
                let result = try await doSomething()
                print("Result = \(result)")
            }catch{
                if let whatError = error as? BigError{
                    print("This is the error = \(whatError)")
    
                }else{
                    print("Unknown error")
                }
            }
        }
        print("Ending task")
    }
    
    callFunction()
    let endTime = NSDate()
    print("Completed in \(endTime.timeIntervalSince(startTime as Date)) seconds")
    

     

  3. Переведем курсор на точку слева от последней строки и нажмем кнопку Run. Мы увидим следующий результат:
Starting task

Ending task

Completed in 0.010606050491333008 seconds

Result = success

Лично мое мнение как переводчика- параллельное программирование с использованием ключевых слов async/await представляет собой более логичное и простое к пониманию оформление параллельного кода, чем использование допотопных обработчиков. А теперь посмотрим на вывод нашей псевдо программы. Сначала наша функция doSomething() выводит на экран надпись “Starting task”. Затем функция callFunction() выводит на экран надпись “Ending task”. Почему же результат выводится после того, как функция callFunction() завершится? Все дело в том, что мы запустили асинхронную задачу с функцией doSomething().  Как видно по трассировке вывода, наш параллельный поток пребывает в состоянии сна 2 секунды, а все приложение отрабатывается за 0.01 секунду, и вывод результата отрабатывается только после завершения работы функции doSomething().

Параллельное программирование в Swift при работе с UI

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

Даже редкое подвисание приложения способно разочаровать пользователя и сподвигнуть его на удаление Вашей программы с его устройства. Для этого мы и переносим часть задач в фоновый поток, чтобы пользовательский интерфейс спокойно продолжал работать без лагов. Давайте смоделируем ситуацию подвисания UI при использовании однопоточной архитектуры. Для этого сделаем:

1. Откроем созданное нами приложение Chapter5

2. Откроем исходный файл ContentView в панели Навигатора

3. Добавим 2 State переменные сразу под объявлением ContentView: View

@State var message = ""
@State var sliderValue = 0.0

4. Под объявлением State переменных добавим перечисление:

enum Response{
     case success
}

5. И добавим VStack представление со следующими визуальными компонентами:

VStack {
       Button("Click Me"){
            }
       Spacer()
       Slider(value: $sliderValue)
       Text("Message = \(message)")
}

6. Добавим кнопке следющеий код:

let startTime = NSDate()
Thread.sleep(forTimeInterval: 10)
callFunction()
let endTime = NSDate()
message = "Completed in \(endTime.timeIntervalSince(startTime as Date)) seconds"

Наиболее важная часть кода здесь заключается в том, что мы останавливаем поток выполнения на 10 секунд(отправляем в спячку). Это служит своего рода эмуляцией трудоемкой задачи, которая зависает наше приложение.

7. Добавим следующие 2 инструкции в классе ContentView:

func doSomething() async throws -> Response{
    return Response.success
}
    
func callFunction(){
    Task(priority: .high){
        do{
            _ = try await doSomething()
        }catch{
                //
        }
    }
}

Функция callFunction() не делает ничего, кроме как вызывает другую функцию doSomething(), используя ключевое слово await. Вызов функции с ключевым словом await работает только если вызываемая функция определена с ключевым словом async. Итоговый код выглядит так:

import SwiftUI

struct ContentView: View {
    @State var message = ""
    @State var sliderValue = 0.0
    
    enum Response{
        case success
    }
    
    var body: some View {
        VStack {
            Button("Click Me"){
                let startTime = NSDate()
                Thread.sleep(forTimeInterval: 10)
                callFunction()
                let endTime = NSDate()
                message = "Completed in \(endTime.timeIntervalSince(startTime as Date)) seconds"
            }
            Spacer()
            Slider(value: $sliderValue)
            Text("Message = \(message)")
        }
    }
    
    func doSomething() async throws -> Response{
        return Response.success
    }
    
    func callFunction(){
        Task(priority: .high){
            do{
                _ = try await doSomething()
            }catch{
                //
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

8. Нажмем на иконку Preview на панели Canvas.

9. Нажмем на кнопку Click Me. Попробуем поиграть с интерфейсом – как мы увидим, слайдер завис намертво на все время работы задачи, а это 10 секунд. И только после окончания выполнения задачи наш UI снова оживает.

Параллельное программирование в Swift sync await

10. Снова нажмем на иконку Preview для выключения режима предварительного просмотра.

11. Закомментируем строку Thread.sleep(forTimeInterval: 10), расположенную в блоке кнопки.

12. Добавим этот же код в функцию doSomething():

func doSomething() async throws -> Response{
    Thread.sleep(forTimeInterval: 10)
    return Response.success
}

Измененный код ContentView файла должен теперь выглядеть так:

import SwiftUI

struct ContentView: View {
    @State var message = ""
    @State var sliderValue = 0.0
    
    enum Response{
        case success
    }
    
    var body: some View {
        VStack {
            Button("Click Me"){
                let startTime = NSDate()
//                Thread.sleep(forTimeInterval: 10)
                callFunction()
                let endTime = NSDate()
                message = "Completed in \(endTime.timeIntervalSince(startTime as Date)) seconds"
            }
            Spacer()
            Slider(value: $sliderValue)
            Text("Message = \(message)")
        }
    }
    
    func doSomething() async throws -> Response{
        Thread.sleep(forTimeInterval: 10)
        return Response.success
    }
    
    func callFunction(){
        Task(priority: .high){
            do{
                _ = try await doSomething()
            }catch{
                //
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

13. Нажмите иконку Preview на панели Canvas.

14. Нажмите на кнопку Click Me. Теперь несмотря на то, что мы ставим поток в ожидание на 10 секунд, приложение продолжает работать в стационарном режиме. Это происходит потому что мы вынесли код, отвечающий за “засыпание” потока в отдельную асинхронную функцию. И наше приложение спокойно продолжает работать и взаимодействовать с пользователем.

Заключение

Главная идея параллельного выполнения кода заключается в том, чтобы Ваше приложение никогда не зависало во время выполнения программного кода, словно Ваша программа сломалась. Первое решение заключается в использовании обработчиков обратного вызова, срабатывающих в то время, когда Ваша функция закончила работу. Но при этому у нас страдает чистота кода, так как если в вашем классе 5-6 обработчиков обратного вызова, начинается логическая неразбериха. Такой код сложен в чтении и понимании.

В последних версиях Swift добавили новый функционал, реализующий простой подход к созданию параллельного кода с использованием ключевых слов async/await. Ключевое слово await используется для вызова асинхронной функции, которая должна быть определена ключевым словом async. Таким образом использование async/await призвано помочь в запуске асинхронного кода, чтобы код отвечающий за пользовательский интерфейс, никогда не был затронут.

Leave a Reply

Please disable your adblocker or whitelist this site!