Как устроена многопоточность в разных языках



В этом посте упрощённо опишу, как происходит работа с потоками в разных языках. Для сравнения возьму 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