Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Stackless и stackful? Корутины и асинхронность ...

Lamoda Tech
December 20, 2024

Stackless и stackful? Корутины и асинхронность в Go

Дима Буров, старший Go-разработчик Lamoda Tech

Асинхронность — важный элемент современных систем, и корутины играют в этом не последнюю роль, но как насчёт Go? В этом докладе мы поговорим о том, что такое корутины, как они работают, и в чём их отличие от горутин. Обсудим stackless и stackful корутины, их плюсы и минусы, и главное, попробуем разобраться, как создать корутинное поведение на Go. Разберём, когда это полезно, какие есть ограничения и почему Go всё-таки выбрал свой путь к асинхронности.

Lamoda Tech

December 20, 2024
Tweet

More Decks by Lamoda Tech

Other Decks in Programming

Transcript

  1. • 10+ лет в коммерческой разработке • Community Lead Go-сообщества

    в Lamoda Tech • Постиг самообразование. Верстал html, css, писал на JS, PHP, Golang • Руководил отделом рекламы (не понравилось) • Неравнодушен к open-source Cвичнулся из военных дирижеров :) Обо мне
  2. Краткий ввод в теорию. Асинхронность и её роль в современных

    высоконагруженных системах Корутины. Их концепция и реализации. Stackless и Stackful подходы, плюсы и минусы О чем доклад 1 2 Асинхронность в Go. Горутины и их реализация, являются ли они корутинами или выбрали свой путь 3 Классическое корутинное поведение в Go. Когда оправдано, а когда лучше обойтись встроенными средствами языка 4
  3. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г.
  4. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г. большинство серверов работали на модели “один поток на соединение”
  5. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г. большинство серверов работали на модели “один поток на соединение” аппаратное ограничение и ограничение операционных систем
  6. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г. масштабирование упиралось в ресурсы: память и процессорное время большинство серверов работали на модели “один поток на соединение” аппаратное ограничение и ограничение операционных систем
  7. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г. масштабирование упиралось в ресурсы: память и процессорное время большинство серверов работали на модели “один поток на соединение” блокирующий ввод-вывод аппаратное ограничение и ограничение операционных систем
  8. Историческая проблема 10 000 клиентов Сервер Проблема 10 000 соединений

    (C10k, Concurrency 10,000) - проблема, обозначающая сложность обработки 10,000 одновременных соединений сервером. Обозначена Деном Кегелем в 1999 г. масштабирование упиралось в ресурсы: память и процессорное время большинство серверов работали на модели “один поток на соединение” блокирующий ввод-вывод аппаратное ограничение и ограничение операционных систем
  9. Историческая проблема Проблема привела к разработке более эффективных подходов: Неблокирующий

    ввод-вывод Появились API, такие как epoll (Linux), kqueue (FreeBSD) и IOCP (Windows), которые оптимизированы для большого числа соединений 1
  10. Историческая проблема Проблема привела к разработке более эффективных подходов: Неблокирующий

    ввод-вывод Появились API, такие как epoll (Linux), kqueue (FreeBSD) и IOCP (Windows), которые оптимизированы для большого числа соединений 1 Асинхронное программирование Программисты начали использовать неблокирующие операции и модели событий (libevent, libuv) 2
  11. Историческая проблема Проблема привела к разработке более эффективных подходов: Неблокирующий

    ввод-вывод Появились API, такие как epoll (Linux), kqueue (FreeBSD) и IOCP (Windows), которые оптимизированы для большого числа соединений 1 Асинхронное программирование Программисты начали использовать неблокирующие операции и модели событий (libevent, libuv) 2 Легковесные модели Вместо потоков стали использовать корутины, зеленые потоки и другие механизмы для экономии ресурсов 3
  12. Историческая проблема На сегодня, проблема перешла в C100k и даже

    C1M, так как современные системы справляются с миллионами соединений благодаря улучшенным алгоритмам, не блокирующему вводу- выводу и масштабируемым архитектурам.
  13. Асинхронность, почему она важна? Асинхронность — это модель выполнения программ,

    при которой операции выполняются без блокировки основного потока исполнения. Ввелась возможность работы с тысячами соединений, используя неблокирующий ввод/вывод (non-blocking IO). Выполнение длительной операции Параметры длительной операции Результат длительной операции Основной поток
  14. Парадигмы задач IO-bound Задачи, которые в первую очередь ограничены операциями

    ввода/вывода, такими как чтение или запись данных на устройства хранения или взаимодействие с внешними системами. Эти задачи тратят значительное количество времени на ожидание чтения или записи данных на внешние источники, такие как дисководы, сети или базы данных. SERVICE DB slow I/O call
  15. Парадигмы задач IO-bound Задачи, которые в первую очередь ограничены операциями

    ввода/вывода, такими как чтение или запись данных на устройства хранения или взаимодействие с внешними системами. Эти задачи тратят значительное количество времени на ожидание чтения или записи данных на внешние источники, такие как дисководы, сети или базы данных. CPU-bound Задачи, которые в первую очередь ограничены вычислительной мощностью центрального процессора. Задачи подразумевают выполнение значительных вычислений, таких как математические расчеты, обработка данных или сложные алгоритмы. SERVICE DB slow I/O call CPU
  16. Парадигмы задач IO-bound Задачи, которые в первую очередь ограничены операциями

    ввода/вывода, такими как чтение или запись данных на устройства хранения или взаимодействие с внешними системами. Эти задачи тратят значительное количество времени на ожидание чтения или записи данных на внешние источники, такие как дисководы, сети или базы данных. CPU-bound Задачи, которые в первую очередь ограничены вычислительной мощностью центрального процессора. Задачи подразумевают выполнение значительных вычислений, таких как математические расчеты, обработка данных или сложные алгоритмы. Memory-bound SERVICE DB slow I/O call CPU
  17. Событийно-ориентированный Основывается на использовании событий и обработчиков для управления процессами.

    Приложение имеет главный цикл (event loop), который отслеживает поступающие события (ввод/вывод, сообщения) и передает их обработчикам. Асинхронные подходы
  18. Событийно-ориентированный Основывается на использовании событий и обработчиков для управления процессами.

    Приложение имеет главный цикл (event loop), который отслеживает поступающие события (ввод/вывод, сообщения) и передает их обработчикам. Асинхронные подходы Потоки (Multithreading) Каждый выполняемый процесс получает свою собственную цепочку инструкций и стек, что позволяет выполнять несколько задач параллельно. Потоки поддерживаются на уровне операционной системы и часто ассоциируются с моделью shared memory для взаимодействия.
  19. Событийно-ориентированный Основывается на использовании событий и обработчиков для управления процессами.

    Приложение имеет главный цикл (event loop), который отслеживает поступающие события (ввод/вывод, сообщения) и передает их обработчикам. Асинхронные подходы Потоки (Multithreading) Каждый выполняемый процесс получает свою собственную цепочку инструкций и стек, что позволяет выполнять несколько задач параллельно. Потоки поддерживаются на уровне операционной системы и часто ассоциируются с моделью shared memory для взаимодействия. Корутины (Coroutines) Легкие, управляемые пользователем потоки выполнения, которые позволяют приостанавливать выполнение задачи и продолжать её позже. Они выполняются в рамках одного или нескольких потоков, переключение между ними управляется на уровне языка или рантайма, а не ОС.
  20. Проблема потоков Обычно потоки реализуются на уровне операционной системы (потоки

    ядра), но у этого подхода есть недостатки: Высокая стоимость переключения контекста Переключение между потоками требует сложных операций и занимает много ресурсов, что замедляет выполнение программы. 1
  21. Проблема потоков Обычно потоки реализуются на уровне операционной системы (потоки

    ядра), но у этого подхода есть недостатки: Высокая стоимость переключения контекста Переключение между потоками требует сложных операций и занимает много ресурсов, что замедляет выполнение программы. 1 Зависимость от ядра Если поток блокируется, то ядро передает управление другому потоку. Эффективность зависит от того, насколько хорошо ядро управляет потоками. 2
  22. Проблема потоков Обычно потоки реализуются на уровне операционной системы (потоки

    ядра), но у этого подхода есть недостатки: Высокая стоимость переключения контекста Переключение между потоками требует сложных операций и занимает много ресурсов, что замедляет выполнение программы. 1 Зависимость от ядра Если поток блокируется, то ядро передает управление другому потоку. Эффективность зависит от того, насколько хорошо ядро управляет потоками. 2 Ограниченная масштабируемость Если процесс создает много потоков, это увеличивает нагрузку на операционную систему и её планировщик, что может привести к снижению производительности. 3
  23. Определение корутины Корутина — это обобщенный вызов функции, который может

    быть приостановлен и возобновлен в произвольный момент времени. Вместо передачи управления операционной системе (как в потоках), корутины управляются на уровне пользователя или языка программирования. Запущена Остановлена Thread 1 Thread 2 Возобновлена Возобновлена Остановлена Остановлена Возобновлена Корутина
  24. Модели мультиплексирования 1:1 1:N M:N Ядро Пользовательское пространство Поток ОС

    Корутина Поток ОС Корутина Корутина Поток ОС Корутина Корутина Поток ОС
  25. Stackful корутина Stackful — реализация, при которой каждая корутина имеет

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

    собственный стек вызовов, что позволяет им сохранять состояние не только на текущем уровне выполнения, но и в вызовах, происходящих глубже в цепочке. + Гибкость + Простота отладки, благодаря стектрейсу + Динамическая глубина вызовов + Подходит для сложных сценариев
  27. Stackful корутина Stackful — реализация, при которой каждая корутина имеет

    собственный стек вызовов, что позволяет им сохранять состояние не только на текущем уровне выполнения, но и в вызовах, происходящих глубже в цепочке. + Гибкость + Простота отладки, благодаря стектрейсу + Динамическая глубина вызовов + Подходит для сложных сценариев - Затраты памяти на стек - Сложность управления стеком - Не подходит для легковесных задач
  28. Stackful: фиксированный стек Размер стека задается при создании корутины и

    не меняется в процессе ее работы. + Простота реализации Управление памятью проще, так как размер предопределен + Прогнозируемость Нет накладных расходов на изменения размера стека + Быстрая работа Нет необходимости копировать данные при расширении
  29. Stackful: фиксированный стек Размер стека задается при создании корутины и

    не меняется в процессе ее работы. + Простота реализации Управление памятью проще, так как размер предопределен + Прогнозируемость Нет накладных расходов на изменения размера стека + Быстрая работа Нет необходимости копировать данные при расширении - Риск переполнения Если размер стека окажется недостаточным для выполнения задач, программа может аварийно завершиться (stack overflow) - Избыточное потребление памяти Если стек был выделен слишком большой, то пользователь может и не использовать всю выделенную память, тратя ресурсы впустую
  30. Stackful: динамический стек Размер стека изменяется динамически, увеличиваясь или уменьшаясь

    в зависимости от текущих потребностей корутины. + Экономия памяти Корутине выделяется ровно столько памяти, сколько нужно для выполнения текущей задачи + Масштабируемость Удобен для большого числа корутин, так как нет необходимости резервировать большой объём памяти для всех корутин + Отсутствие ограничений размера Меньший риск переполнения (если позволяет объем доступной памяти)
  31. Stackful: динамический стек Размер стека изменяется динамически, увеличиваясь или уменьшаясь

    в зависимости от текущих потребностей корутины. + Экономия памяти Корутине выделяется ровно столько памяти, сколько нужно для выполнения текущей задачи + Масштабируемость Удобен для большого числа корутин, так как нет необходимости резервировать большой объём памяти для всех корутин + Отсутствие ограничений размера Меньший риск переполнения (если позволяет объем доступной памяти) - Накладные расходы Изменение размера стека может быть дорогостоящим из-за копирования данных в новую область памяти - Сложность реализации Требуется логика для управления расширением и сокращением стека - Проблема фрагментации Стек может сильно фрагментироваться, если состоит из множества маленьких сегментов, разбросанных по всей памяти
  32. Stackful: пример yield() Coroutine resume() Восстанавливается полный стек выполнения, контекст

    и исполнение продолжается точно с той точки, где произошло yield()
  33. Stackless корутина Stackless — подход, при котором корутина не использует

    собственный стек вызовов, а управляется с помощью структуры данных, представляющей состояние выполнения. Вместо сохранения полной стека они сохраняют минимальный контекст, необходимый для возобновления работы.
  34. Stackless корутина Stackless — подход, при котором корутина не использует

    собственный стек вызовов, а управляется с помощью структуры данных, представляющей состояние выполнения. Вместо сохранения полной стека они сохраняют минимальный контекст, необходимый для возобновления работы. + Экономия памяти + Простота переключения + Идеальны для простых IO-bound задач
  35. Stackless корутина Stackless — подход, при котором корутина не использует

    собственный стек вызовов, а управляется с помощью структуры данных, представляющей состояние выполнения. Вместо сохранения полной стека они сохраняют минимальный контекст, необходимый для возобновления работы. + Экономия памяти + Простота переключения + Переносимость под платформы и архитектуры - Сложность отладки - Нельзя использовать рекурсию напрямую - Управление большой иерархией вызовов становится громоздким
  36. Stackless: пример Корутины Event loop Очередь задач Мониторинг очереди задач

    Запуск задач, пока блокировка IO не приостановит их через ОС Проверка завершения IO и возобновление задачи ОС Обработка задач IO Уведомление о завершении задачи IO
  37. Горутины Go предлагает уникальную реализацию асинхронности через Горутины, которые все

    же отличаются от классических корутин и потоков. Почему горутины считаются легкими?
  38. Горутины Go предлагает уникальную реализацию асинхронности через Горутины, которые все

    же отличаются от классических корутин и потоков. Почему горутины считаются легкими? Минимальный начальный стек Горутины начинают выполнение с минимального стека (2-4 КБ), который растёт по мере необходимости. Это резко контрастирует с потоками, где стек может занимать мегабайты
  39. Горутины Go предлагает уникальную реализацию асинхронности через Горутины, которые все

    же отличаются от классических корутин и потоков. Почему горутины считаются легкими? Минимальный начальный стек Горутины начинают выполнение с минимального стека (2-4 КБ), который растёт по мере необходимости. Это резко контрастирует с потоками, где стек может занимать мегабайты Экономия ресурсов Создание горутины обходится значительно дешевле, чем поток. Переключение между горутинами также быстрее благодаря управлению на уровне среды выполнения
  40. Горутины Go предлагает уникальную реализацию асинхронности через Горутины, которые все

    же отличаются от классических корутин и потоков. Почему горутины считаются легкими? Минимальный начальный стек Горутины начинают выполнение с минимального стека (2-4 КБ), который растёт по мере необходимости. Это резко контрастирует с потоками, где стек может занимать мегабайты Экономия ресурсов Создание горутины обходится значительно дешевле, чем поток. Переключение между горутинами также быстрее благодаря управлению на уровне среды выполнения Динамическое масштабирование Горутины могут расти или сокращаться в зависимости от нагрузки, что делает их идеальными для высоконагруженных приложений
  41. Горутины Модель планирования M:N Горутины (M) сопоставляются с меньшим количеством

    потоков (N). Это позволяет тысячам горутин эффективно работать на нескольких ядрах Планировщик (Goroutine Scheduler) Отвечает за распределение выполнения горутин на потоки ОС по GMP-модели. Переход между Горутинами осуществляется внутри потока ОС, что позволяет избежать затрат на переключение контекста Параллелизм Горутины могут выполняться в нескольких потоках операционной системы. Они продвигают детерминированный параллелизм (например, каналы с одним отправителем и одним получателем) Вытесняющая многозадачность (Preemptive Scheduling) С версии Go 1.14 планировщик поддерживает асинхронно вытесняющее переключение горутин, предотвращая “захват” процессора одной горутиной
  42. Горутина это Корутина? Да, но… Горутины часто сравнивают с классическими

    Корутинами, однако у них есть как общие черты, так и фундаментальные отличия.
  43. Горутина vs корутина Горутина Stackful, Lua Stackless, Python Подходящие задачи

    IO-bound и CPU-bound IO-bound IO-bound, частично CPU-bound Стек Динамически растущий Фиксированный или динамический стек Отсутствует Планировщик Встроенный M:N Отсутствует На уровне runtime Передача управления Неявная Явная Явная Параллелизм Да Нет Нет Прямой блокирующий вызов Да Нет Нет Преемптивное переключение Да Нет Нет Синхронизация Да Нет Частично
  44. Классические корутины в Go Russ Cox, Coroutines for Go, 2023

    https://research.swtch.com/coro Go не предоставляет корутины в классическом понимании. В Go нет возможности просто так “приостановить” выполнение функции и затем “возобновить” ее с той же точки стека.
  45. Классические корутины в Go Russ Cox, Coroutines for Go, 2023

    https://research.swtch.com/coro Go не предоставляет корутины в классическом понимании. В Go нет возможности просто так “приостановить” выполнение функции и затем “возобновить” ее с той же точки стека. Есть последовательность операций, которую вы хотите выполнить частично, приостанавливаясь и возобновляясь по требованию
  46. Классические корутины в Go Russ Cox, Coroutines for Go, 2023

    https://research.swtch.com/coro Go не предоставляет корутины в классическом понимании. В Go нет возможности просто так “приостановить” выполнение функции и затем “возобновить” ее с той же точки стека. Есть последовательность операций, которую вы хотите выполнить частично, приостанавливаясь и возобновляясь по требованию Парсер со сложной логикой, который мог бы парсить данные по кускам: вы читаете часть данных, “yield”, отдаёте управление вызывающему коду, тот загружает еще данные и вызывает “resume” и парсер продолжает с того места, где остановился
  47. Классические корутины в Go Russ Cox, Coroutines for Go, 2023

    https://research.swtch.com/coro Go не предоставляет корутины в классическом понимании. В Go нет возможности просто так “приостановить” выполнение функции и затем “возобновить” ее с той же точки стека. Есть последовательность операций, которую вы хотите выполнить частично, приостанавливаясь и возобновляясь по требованию Парсер со сложной логикой, который мог бы парсить данные по кускам: вы читаете часть данных, “yield”, отдаёте управление вызывающему коду, тот загружает еще данные и вызывает “resume” и парсер продолжает с того места, где остановился Если вы моделируете сложные состояния игры/программы, где важно вручную управлять шагами выполнения в ответ на внешние события
  48. Классические корутины в Go: что еще? Использовать range over function

    Возможно расширить код с использованием функций-итераторов. Но иногда потребуется выход за рамки одного цикла `for`, например чередование нескольких итераций https://clck.ru/3FAmWe
  49. Классические корутины в Go: что еще? Использовать range over function

    Возможно расширить код с использованием функций-итераторов. Но иногда потребуется выход за рамки одного цикла `for`, например чередование нескольких итераций Оптимизация Переключения между корутинами, есть узкие места, например каналы, которые тоже имеют атомарные операции, которые влияют на производительность и не только. https://clck.ru/3FAmWe
  50. Классические корутины в Go: что еще? Использовать range over function

    Возможно расширить код с использованием функций-итераторов. Но иногда потребуется выход за рамки одного цикла `for`, например чередование нескольких итераций Оптимизация Переключения между корутинами, есть узкие места, например каналы, которые тоже имеют атомарные операции, которые влияют на производительность и не только. https://clck.ru/3FAmWe Паника и отмена корутин Передача паники вызывающей стороне, которая последний раз вызывала Resume(), а также сигнал отмены корутины для вызывающей стороны
  51. Классические корутины в Go: что еще? Использовать range over function

    Возможно расширить код с использованием функций-итераторов. Но иногда потребуется выход за рамки одного цикла `for`, например чередование нескольких итераций Оптимизация Переключения между корутинами, есть узкие места, например каналы, которые тоже имеют атомарные операции, которые влияют на производительность и не только. https://clck.ru/3FAmWe Паника и отмена корутин Передача паники вызывающей стороне, которая последний раз вызывала Resume(), а также сигнал отмены корутины для вызывающей стороны Тесты, бенчмарки, сравнительный анализ Неплохо было бы протестировать код, чтобы сравнить производительность и эффективность данного подхода в разных корнер-кейсах
  52. Заключение Асинхронное программирование и Корутины — отлично решают проблемы современных

    высоконагруженных систем 1 Горутины — это особый вид Корутин. Не просто асинхронный инструмент — это мощный автоматизированный механизм, объединяющий простоту использования Корутин с эффективностью потоков 2
  53. Заключение Асинхронное программирование и Корутины — отлично решают проблемы современных

    высоконагруженных систем 1 Горутины — это особый вид Корутин. Не просто асинхронный инструмент — это мощный автоматизированный механизм, объединяющий простоту использования Корутин с эффективностью потоков 2 Реализация классического корутинного поведения в Go может быть полезной для создания более специфичных решений, где Горутин недостаточно из-за ограничений их модели планировщика 3