Всем привет, на связи Никита и Технократия! В прошлой статье мы уже обсудили проблемы текущего состояния 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 Разработчик Технократии
Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.
DevilDimon
Как введение статья хорошая, однако разбор понятий concurrency и parallelism здесь приделан как-то сбоку и вообще непонятно, зачем нужен.
То же самое про корутины. В официальной документации и литературе по теме (Modern Swift Concurrency от Ray Wenderlich, например) этот термин не используется.
В голове от этого может сложиться ложное впечатление о связи свифтовского async/await с корутинами в Go и Kotlin, когда как на деле кроме синтаксиса в них мало что похоже.
technokratiya Автор
Разбор определений нужен был для более подробного описания модели и для использования этих терминов в будущих статьях.
В первом абзаце есть ссылка на proposal, в котором упоминается о использовании данной модели в Swift 5.5 Если обратиться к википедии, то мы увидим следующую терминологию:
"Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed."
Swift с новой поточкой подходит под все характеристики, а именно: У нас non-preemptive multitasking, мы можем приостанавливать задачи и продолжать их выполнение.
Возможно, мы ошибаемся, тогда хотелось бы услышать, в чём фундаментально заключается отличие async/await от Kotlin.
Никита Сосюк, автор статьи
DevilDimon
В официальной документации, сессиях WWDC и книге SPL термин "корутина" не встречается. Думаю, неспроста. Не сбивайте людей, потому что корутины в Свифте официально так не называются.
Википедию может редактировать кто угодно, это не аргумент. Многозадачность у них все-таки вытесняющая - в любой момент выполняющийся на ядре код может быть приостановлен операционной системой. Не забывайте, что Darwin не DOS. Вытеснения нет только от других таких же "partial tasks", и кооперация у них только между собой, а не на уровне потоков ОС.
Фундаментального отличия от Kotlin нет, но есть мелкие, которые очень важны - компиляция (аналога Sendable, насколько я понял, нет, то есть компилятор полезть в shared state запретить не сможет, особенно если через java/инструментацию), в Свифте нельзя управлять Executor'ами, в отличие от котлина (попробуйте, например, заставить таски выполняться на нужных потоках в нужном порядке - в Свифте это сейчас невозможно), ну и вместо каналов используется совсем иная идея с акторами.
Может, в чем-то неточен, на котлине ежедневно не пишу, но в целом различия для меня достаточно разительные и влияют на ежедневную мобильную разработку непосредственно - точно управлять потоками в тестах, например, необходимо очень часто. Вы пишете "мы можем приостанавливать задачи и продолжать их исполнение", но на деле в Свифте "мы" мало что можем, все partials отдаются компилятору как есть и влияние на них программиста ограничивается cancel'ом и yield'ом. В котлине поближе к народу все-таки.
NikitaShelov
Мы рады прислушиваться к Вашим комментариям, чтобы дополнять или исправлять статью. Мы исправили название статьи, убрав слово «Swift», чтобы не вводить Вас в заблуждение.
Однако есть пару моментов:
1. Согласно propasal новая поточка базируется (берет основы) этой модели. В рамках данной статьи мы рассказали именно о принципах работы модели корутин. Четкой аналогии с языком Swift я не проводил и не называл асинхронные функциями в Swift корутинами.
2. Давайте все-таки не спускаться до устройства операционной системы, чтобы оставить низкий порог входа у статьи. Мною сделан вывод о том, что новая поточка реализует cooperative multitasking, исходя из того, что мы сами указываем, в какой момент выполнения может остановиться наша функция. Безусловно, система сама управляет потоками, решать таким моменты у разработчика нет возможности. Однако раньше мы могли лишь гадать, а сейчас указываем эти места благодаря новому синтаксису ????
3. Сама модель общая (остановка и продолжение выполнения задач, стек вызовов и т.д.) Различия, безусловно, есть. Решения разных языков не идентичны и на это есть свои причины. Специфику Swift мы разберем в следующих статьях (Sendable и т.п).
Сама статья как раз подводит к тому, как реализована работа с async/await в Swift, а именно к реализации Task и всему соответствующему.
Предлагаю дождаться следующей статьи и обсудить там.
Спасибо за комментарий!
DevilDimon
Благодарю за ответ! Жду следующих статей. В интернете сейчас мало информации по теме, за пределами простейших примеров мало кто ходит, а в продакшене и того меньше можно встретить.