Pull to refresh

[Async/await] Как работает Coroutine Model

Reading time5 min
Views6.6K

Всем привет, на связи Никита и Технократия! В прошлой статье мы уже обсудили проблемы текущего состояния 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-ом. 

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

Таким образом: 

  1.  Становится легче писать асинхронный код.

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

  3. Появляется гибкая многозадачность.

  4. Уменьшается количество смены исполняемых Thread-ов, context switch.

  5. Уменьшается количество использований механизмов синхронизации, блокирующих поток.

Вывод

Мы разобрались с устройством новой для Swift модели корутин, которая, выражаясь изученными терминами, реализует cooperative multitasking и позволяет работать с задачами конкурентно, так и параллельно.

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

Никита Сосюк

iOS Разработчик Технократии

Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.

Tags:
Hubs:
Total votes 5: ↑4 and ↓1+3
Comments5

Articles