Swift 5.5 представляет собой новую модель управления асинхронным программированием. Хотя многие ожидали анонса нового оборудования на выставке Apple WWDC21, вместо этого мы получили сокращение числа людей, которые будут писать код для приложений на основе Swift, включая iOS и другие. Как мы увидим, эти изменения направлены на реализацию более структурированного кода, когда дело касается асинхронных операций.

Праймер - Структурированные переменные

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

var item = "first"
item = nil  //nil cannot be assigned to type String

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

Optionals улучшили всю экосистему приложения, предоставив разработчикам базовые правила, которых они могут придерживаться во время компиляции. Перенесемся в сегодняшний день, и концепция структурированного кодирования превратилась в другой важный аспект разработки iOS - многопоточные операции. Многие называют эту новую парадигму, известную как структурированный параллелизм, как async / await. Давайте рассмотрим, как это работает.

Актеры

Swift 5.5 представляет новый тип, который обеспечивает синхронизацию общего изменяемого состояния вместе со структурами и классами. Эти ссылочные типы, называемые акторами, изолируют свое состояние от остальной части программы. В терминологии Objective-c это атомарное поведение. Наличие модели, которая обеспечивает этот тип изоляции (акторов), имеет решающее значение при разработке модели для защиты от состояний гонки, часто встречающихся в параллельных операциях:

Actor Counter {  //atomic!
    var value = 1
        
    func increment() -> Int {
        value += 1
        return value
    }
    
    //there's no need to mark internal methods or properties
    //as async since they are all running within the same 
    (protected scope)
    
    func increase() -> Int {
        return self.increment()
    }
    
    
    func multiply(_ sum: Int) -> Int {
        
        let m = Multiplier()
        let product = m.double(using: sum)
        return product
        
    }
    
}

extension Counter {
    func decrement() -> Int {
        value -= 1
        return value
    }
}

Их атомарное поведение обеспечивает определенный уровень соответствия при доступе к их методам или свойствам. На первый взгляд код выглядит стандартным синтаксисом Swift. Однако следует отметить, что Counter имеет полное право изменять состояние своего внутреннего свойства (значения). Этот дизайн проявляется в реализации методов увеличения () и декремента (), реализованных в расширении типа Actor. Поддерживая принцип инкапсуляции, изменение данных для Актеров может происходить только внутри типа. Это известно как изоляция актеров. Чтобы получить более четкое представление о том, как это работает, давайте создадим модульный тест для проверки функции счетчика.

Работа с актерами

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

func testWithActors()  {
        
        let c = Counter()
                
        async {
            let sum = await c.increment()
            let product = await c.multiply(sum)
            await printResults(using: product)
        }        
                
        print("called before async..")
    }
    
    
    //present content results on the main thread..
   @MainActor func printResults(using item: Int)

Для начала обратите внимание, что testWithActors () вызывается как обычная синхронная функция. При разработке приложений общее правило состоит в том, что взаимодействия для основного пользовательского интерфейса всегда происходят в основном потоке. Типичный сценарий: приложение начинает свое основное выполнение в основном потоке, а затем получает сегмент кода или обрабатывает некоторые данные во вторичной фоновой задаче. Это может быть что-то простое, например получение изображений из службы на основе REST или запуск фонового задания.

После объявления нового экземпляра Counter мы пытаемся получить информацию от Актера, вызывая c.increment () и присваивая это возвращаемое значение сумме. Как показано, этот код заключен в новое закрытие async {}, известное как неструктурированная задача. Как мы знаем, замыкания работают как функции, которые можно использовать в других процессах. Еще одна полезная функция заключается в том, что замыкания выполняются в пределах своей (отдельной) области видимости и захватывают значения из окружающего их контекста.

В рамках нашей модели обратите внимание, что мы не можем просто вызвать c.increment (), но должны префикс этого оператора с помощью await. Новое в Swift 5.5 ключевое слово await указывает, что вторичному / фоновому потоку, возможно, придется приостановить свое выполнение, чтобы удовлетворить требования эксклюзивности актора. В результате компилятор проверяет требуемое согласованное поведение во время компиляции. Чтобы проиллюстрировать это, давайте попробуем реорганизовать код, чтобы он работал вне задачи async {}.

func testWithActors()  {
        
        let c = Counter()

        //actor-isolated instance can only be accessed within 
        //the actor..
        let sum = await c.increment() //compilation error

        ...
 }

Главный Актер

Последний шаг в нашей задаче async {} - распечатать результаты, полученные от Актера. Однако предположим, что мы хотим представить этот контент в основном потоке. Обычно это делается путем обертывания нашего кода закрытием Dispatch.queue.main (). Несмотря на гибкость и удобство, это шаблон проектирования среды выполнения, который может легко вызвать ошибку или сбой, если он будет реализован неправильно. В Swift 5.5 появилась концепция главного действующего лица.

Это ключевое слово, добавленное в качестве атрибута на уровне функции или класса, гарантирует выполнение кода в основном потоке. Подобно работе с Актерами, соответствие Главного Актера также проверяется во время компиляции. В нашем случае этот шаблон проектирования работает в наших интересах. Обратите внимание, как вызов printResults () будет происходить в основном потоке (например, Thread 1), даже если строка выполнения существует в задаче async {}:

...
	//background thread (detached) task
        async {
            let sum = await c.increment()
            let product = await c.multiply(sum)
            await printResults(using: product)
        }        
                
        print("called before async..")
    }
    
    
    //present content results on the main thread..
   @MainActor func printResults(using item: Int)

Что интересно в новом формате async / await, так это то, что эти ключевые слова могут иметь разное значение в зависимости от того, где они применяются в коде. Чтобы вернуться к нашему примеру кода Counter, обратите внимание на размещение await, используемое при возврате значения из c.increment по сравнению с вызовом printResults. Хотя printResults не является асинхронной функцией, неизвестно, когда мы получим переменную продукта. В результате эта строка выполнения должна дождаться своих результатов.

Что дальше

Хотя мы рассмотрели основы, есть некоторые дополнительные аспекты новой структурированной модели параллелизма, включая синхронизацию, задачи и группы задач, asyncSequence, обработку исключений и протокол Sendable. Поскольку Xcode 13 в настоящее время находится в стадии бета-тестирования и ожидается, что он будет выпущен этой осенью, несомненно, есть еще много чего для изучения, но видеть эти долгожданные изменения в экосистеме iOS - это потрясающе. Удачного кодирования!

Понравилась эта статья? Получайте больше подобных материалов в моей еженедельной Лаборатории компьютерных наук для iOS.