Как устроена многопоточность в разных языках
В этом посте упрощённо опишу, как происходит работа с потоками в разных языках. Для сравнения возьму 4 популярных в Европе бэкенд языка — Python, JavaScript, Java и Go.
Начнём с основ.
Любой бэк работает на каком-то железе. Основная цель при разработке — задействовать ресурсы процессора на максимум. Если у процессора 8 ядер, то одновременно могут выполняться 8 задач. Как это достигается:
Python и JavaScript
В этих языках в каждый момент времени выполняется только одна задача. В этом смысле языки можно назвать однопоточными.
Когда задача запускается "в другом потоке", она логически изолируется от текущей. Например, запрос 1 выполняется в потоке Т1, запрос 2 — в потоке Т2. У каждого запроса теперь своя область видимости и локальные переменные.
Эти логические потоки попеременно получают доступ к одному потоку ОС, а значит и к одному ядру.
Один экземпляр сервиса нагружает только одно ядро процессора. Чтобы задействовать 8 ядер, запускают 8 экземпляров сервиса + балансировщик
Плюсы:
✔️ Нет многопоточных проблем
✔️ Код получается линейный, его легко тестировать и дебажить
Минусы:
🙁 Нет общей памяти между сервисами. Для обмена и накопления данных активно используются кэши, месседж брокеры и БД
🙁 Как следствие — чуть более сложная инфраструктура
Java
Потоки в джаве соотносятся с потоками ОС в отношении 1 к 1, поэтому в каждый момент времени может идти работа над 8 задачами (если ядер 8).
Плюсы:
✔️ Один сервис вместо 8 — не нужен дополнительный балансировщик
✔️Общая память между потоками — можно переиспользовать данные и компоненты
✔️ Больше вариантов работы — на выбор даётся классический (thread-per-request) и реактивный стиль. А скоро добавятся виртуальные потоки 🥳
✔️ Шикарная библиотека java.util.concurrent с инструментами на любой вкус
Минусы:
🙁 Код становится сложнее
🙁 Сложно тестировать и дебажить
🙁 Многопоточные сложности: гонки, дедлоки, проблемы с видимостью и атомарностью
🙁 Сложно добиться оптимальной загрузки процессора. Для разных задач нужно подбирать разные параметры и решения
Go
Потоки в go (горутины) соотносятся с потоками ОС как многие ко многим. Сервису нужно гораздо меньше потоков ОС, чем в java.
Благодаря особенностям реализации горутины показывают лучшую производительность в сервисах с большим количеством блокирующих вызовов. В итоге получается отличный микс классической и реактивной архитектуры.
Плюсы:
✔️ Многопоточный код в некоторых случаях проще, чем в джаве
✔️ Один сервис и общая память между потоками
✔️ Отличная работа с блокирующими вызовами
Минусы:
🙁 Всё ещё актуальны многопоточные сложности из пункта про джаву
Это очень базовое описание механизмов работы с потоками. Работающий бэк можно написать на любом языке выше. В тех же Node.js и Django давно есть модули, облегчающие работу с несколькими экземплярами и передачей данных. Но в джаве модель работы с потоками самая сложная и разнообразная.
И правильный ответ на вопрос перед постом: для java соотношение 1:1, для python — N:1
В этом посте упрощённо опишу, как происходит работа с потоками в разных языках. Для сравнения возьму 4 популярных в Европе бэкенд языка — Python, JavaScript, Java и Go.
Начнём с основ.
Любой бэк работает на каком-то железе. Основная цель при разработке — задействовать ресурсы процессора на максимум. Если у процессора 8 ядер, то одновременно могут выполняться 8 задач. Как это достигается:
Python и JavaScript
В этих языках в каждый момент времени выполняется только одна задача. В этом смысле языки можно назвать однопоточными.
Когда задача запускается "в другом потоке", она логически изолируется от текущей. Например, запрос 1 выполняется в потоке Т1, запрос 2 — в потоке Т2. У каждого запроса теперь своя область видимости и локальные переменные.
Эти логические потоки попеременно получают доступ к одному потоку ОС, а значит и к одному ядру.
Один экземпляр сервиса нагружает только одно ядро процессора. Чтобы задействовать 8 ядер, запускают 8 экземпляров сервиса + балансировщик
Плюсы:
✔️ Нет многопоточных проблем
✔️ Код получается линейный, его легко тестировать и дебажить
Минусы:
🙁 Нет общей памяти между сервисами. Для обмена и накопления данных активно используются кэши, месседж брокеры и БД
🙁 Как следствие — чуть более сложная инфраструктура
Java
Потоки в джаве соотносятся с потоками ОС в отношении 1 к 1, поэтому в каждый момент времени может идти работа над 8 задачами (если ядер 8).
Плюсы:
✔️ Один сервис вместо 8 — не нужен дополнительный балансировщик
✔️Общая память между потоками — можно переиспользовать данные и компоненты
✔️ Больше вариантов работы — на выбор даётся классический (thread-per-request) и реактивный стиль. А скоро добавятся виртуальные потоки 🥳
✔️ Шикарная библиотека java.util.concurrent с инструментами на любой вкус
Минусы:
🙁 Код становится сложнее
🙁 Сложно тестировать и дебажить
🙁 Многопоточные сложности: гонки, дедлоки, проблемы с видимостью и атомарностью
🙁 Сложно добиться оптимальной загрузки процессора. Для разных задач нужно подбирать разные параметры и решения
Go
Потоки в go (горутины) соотносятся с потоками ОС как многие ко многим. Сервису нужно гораздо меньше потоков ОС, чем в java.
Благодаря особенностям реализации горутины показывают лучшую производительность в сервисах с большим количеством блокирующих вызовов. В итоге получается отличный микс классической и реактивной архитектуры.
Плюсы:
✔️ Многопоточный код в некоторых случаях проще, чем в джаве
✔️ Один сервис и общая память между потоками
✔️ Отличная работа с блокирующими вызовами
Минусы:
🙁 Всё ещё актуальны многопоточные сложности из пункта про джаву
Это очень базовое описание механизмов работы с потоками. Работающий бэк можно написать на любом языке выше. В тех же Node.js и Django давно есть модули, облегчающие работу с несколькими экземплярами и передачей данных. Но в джаве модель работы с потоками самая сложная и разнообразная.
И правильный ответ на вопрос перед постом: для java соотношение 1:1, для python — N:1