Всем привет, на связи Никита и Технократия! В прошлой статье мы уже обсудили проблемы текущего состояния concurrency в Swift. Давайте двигаться дальше и сегодня мы начнем свое знакомство с необходимой базой для async/await в Swift 5.5
Для начала обратимся к Proposal SE-0296, в котором рассказывают базовые аспекты нового подхода.
Сфокусируемся на одном из первых предложений: «This design introduces a coroutine model to Swift» и заметим новое для большинства iOS-разработчиков словосочетание coroutine model.
Что же это такое? Давайте разбираться.
Терминология
Прежде чем изучать новую модель, давайте сравним пары базовых и очень похожих друг на друга терминов в сфере асинхронного программирования, которые будут упоминаться в будущем:
Multitasking и Multithreading
Multitasking - это возможность программы работать над разным задачами, task-ами, в одну единицу времени.
В рамках операционной системы можно провести аналогию с разными запущенными приложениями на вашем девайсе.
Если рассматривать с точки зрения одного конкретного приложения, то это выполнение программой задач, созданных разработчиками в коде.
На картинке приведен пример с операционной системой, на которой запущены сразу три программы - Pages, Safari, Xcode. Мы можем сказать, что у операционной системы есть 3 задачи, которые она выполняет, что делает ее многозадачной.
Помимо это многозадачность бывает двух видов: preemptive и cooperative.
1. Preemptive - система может остановить выполнение текущего процесса и отдать выполнение другому процессу, в таких случаях происходит вытеснение (preempting).
Пример: Threads, а именно context switch, когда меняется поток, задача приостанавливает свое выполнение в месте, неизвестном для для нас заранее. Проиллюстрируем этот пример. В нашей визуальной интерпретации процессор (система) сам меняет потоки со своими стеками:
2. Cooperative - система останавливает выполнение текущего процесса в заранее известных местах, когда процесс (задача) может «добровольно» отдать управление.
Пример: Новая поточка (coroutine) 😁 Проиллюстрируем по аналогии - здесь наши задачи сами отдают управление системе, которая сама решает, что делать с ней дальше - продолжить выполнение задачи или отложить ее.
Multithreading - это возможность программы исполняться на нескольких потоках, thread-ах, одновременно. Если рассматривать приложение Xcode, то отрисовка UI, компилирование кода, работа с git-ом, все это может происходить на разных потоках, между которыми будет переключаться процессор
Concurrency и Parallelism
Parallelism - возможность программы исполнять несколько независящие друг от друга задачи в один момент времени.
Concurrency - возможность программы выполнять задачи в перекрывающиеся периоды времени.
Проще всего понять их разницу с помощью картинок.
Concurrency:
Parallelism:
Теперь мы разобрались, что Multitasking != Multithreading и Concurrency != Parallelism
История модели корутин
Данную модель впервые использовал в своей конструкции Мелвин Конвей в 1958. Позже она практиковались в некоторых высокоуровневых языках, например, Simula, однако должной популярность данная модель не получила и большинство языков программирования и по сей день используют другую модель - модель Thread-ов, которая знакома большинству программистов.
С течением времени необходимость в параллелизме возрастала, а текущая модель не удовлетворяла по производительности. Таким образом мы и вернулись к новой, но на самом деле, хорошо забытой, модели корутин. На сегодняшний день уже большое количество языков добавила возможность работы с ними. Имплементацию данной технологии можно найти, например, в Kotlin, Scala, C#, а теперь и в Swift.
Устройство
Так как существуют coroutine
, значит, что существуют и routine
. Давайте разберемся сначала с ними.
Routine
- это последовательный вызов инструкций, которые решают конкретную задачу. В рамках Swift это любая функция, которая возвращает управление после того, как выполнится полностью.
Рассмотрим на примере:
func routine() -> Int {
var result = 0
for i in 0...1_000 {
result += i
}
return result
}
У нас есть функция routine
, которая создает переменную, прогоняет цикл 1000 раз и добавляет индекс итерации к числу, после чего возвращает его. Она легка для понимания и, если посмотреть ее низкоуровневую интерпретацию, то выполнение инструкций происходит последовательно, вызовы хранятся на стеке с привязкой к конкретному потоку, на котором началось исполнение.
Coroutine
- это исполняемый код, выполнение которого может быть прервано без блокировки потока, с сохранением полного стека вызова и состояния. После завершение прерывания корутина может продолжить свое выполнение без привязки к изначальному потоку.
У Coroutine
есть, так называемые, suspension points - возможные точки остановки выполнения функции, в Swift они отмечены словом await
. Почему «возможные»? Потому что все зависит от конкретного момента времени, загруженности приложения и выбору Runtime. Мы можем либо ожидать завершение подзадачи, либо решить ее сразу.
Пример
Попробуем решить в лоб задачу с приостановкой выполнения функции. Для этого создадим routineSleep
и coroutineSleep
с новым синтаксисом.
func routineSleep() {
print("Start")
Thread.sleep(forTimeInterval: 10) // Поток "засыпает" на 10 секунд
print("Finish")
}
func coroutineSleep() async {
print("Start")
try? await Task.sleep(nanoseconds: 10_000_000_000) // Задача "засыпает" на 10 секунд
print("Finish")
}
В первом случае мы просто усыпляем поток на 10 секунд, во втором - некую сущность Task
, которую разберем в следующих статьях.
Если запустить первую функцию на main потоке, то интерфейс нашего приложения зафризит на 10 секунд, а во втором случае - нет. Это происходит из-за того, что наша функция routineSleep
потокозависимая, а Thread.sleep
блокирует наш поток для других задач на указанное время.
В случае с корутиной мы просто откладываем дальнейшее выполнение функции на 10 секунд, не блокируя поток. Можно сказать, что мы убираем нашу незавершенную функцию в некий пул задач, с пометкой о необходимости продолжения ее выполнения через 10 секунд. Визуализировав примеры получим:
Можно заметить, что выполнение coroutine может быть продолжено на совершенно другом потоке, в том числе и на стартовом потоке или даже на main, если повезет. Попробуем проверить это и добавим вывод потока до «сна» и после:
func coroutineSleep() async {
print(Thread.current)
try? await Task.sleep(nanoseconds: 10_000_000_000)
print(Thread.current)
}
Запустив данный код, мы получим следующий лог в консоли:
<NSThread: 0x6000020ee100>{number = 5, name = (null)}
<NSThread: 0x6000020322c0>{number = 8, name = (null)}
Да, действительно, в процессе выполнения функции поток был изменен 😁
В чем преимущества над Thread model?
У сoroutine функций свой скоуп, поэтому она не зависят от потока и умеет сохранять состояние, «засыпая» в определенных местах. Благодаря этому мы спокойно может откладывать их выполнение в runtime и запускать их на потоке в нужное для нас время, руководствуясь приоритетом задачи. Этим все занимается обновленный Swift Runtime с Executor-ом.
Пропадает надобность в создании большого количества потоков, т.к. привязка задач к потоку больше нет. В идеале теперь мы можем использовать столько потоков, сколько ядер на нашем девайсе.
Таким образом:
Становится легче писать асинхронный код.
Повышается производительность из-за отсутствия нужды с создание большого количества потоков.
Появляется гибкая многозадачность.
Уменьшается количество смены исполняемых Thread-ов, context switch.
Уменьшается количество использований механизмов синхронизации, блокирующих поток.
Вывод
Мы разобрались с устройством новой для Swift модели корутин, которая, выражаясь изученными терминами, реализует cooperative multitasking и позволяет работать с задачами конкурентно, так и параллельно.
В следующей статье начнем знакомиться с ее имплементацией, познакомимся с синтаксисом async/await
и разберем его на примерах.
Никита Сосюк
iOS Разработчик Технократии
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.