Конкурентность и Параллелизм
Данные понятия вызывают недпонимание у многих разработчиков. Статья поможет вам разобраться с этими концепциями и понять, зачем это нужно.
Понятия конкурентности и параллелизма зачастую вызывают много вопросов и недопонимания у начинающих программистов. Я и сам долгое время использовал конкурентные подходы в работе, не осознавая фундаментальных основ, которые за этим стоят.

Да что там говорить, если вы откроете статью на википедии по запросу "Concurrency", а после этого выберите русский язык, вас перекинет на статью по параллелизму.
Аналогичные ошибки я также встречал в русском переводе книги по языку программирования Go. Подобные некорректные переводы еще больше сбивают с толку и усложняют понимание данного концепта.
Тот материал, который я находил в сети по данной теме, слишком сухой, описан сугубо техническим языком без корректных аналогий и примеров, что не сильно помогает вникнуть в суть.

Поэтому я надеюсь данная статья поможет вам лучше понять эти две концепции и грамотно использовать их при разработке ПО.

А для чего?
Давайте начнем с проблемы, решить которую призваны конкурентность и параллелизм. Современные компьютеры просто обязаны уметь работать с несколькими задачами одновременно.

Например, когда мы смотрим видео на YouTube, мы без проблем можем скроллить страницу и оставить под ним комментарий или открыть новую вкладку и написать какой-нибудь запрос в строке поиска. При этом видео без проблем продолжает воспроизводиться.

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

Все эти примеры объединяет то, что в данных случаях происходит несколько операций одновременно, и при этом они не мешают друг другу.

Согласитесь, было бы очень некомфортно, если видео на ютуб каждый раз ставилось на паузу при скроллинге страницы или написании комментария, поскольку это две разных операции.

Однако архитектура современных компьютеров позволяет нам выполнять множество таких задач одновременно, ну или почти одновременно, об этом мы поговорим чуть позже. И достигается это как раз за счет конкурентности и параллелизма.

Конкурентность
Как вы наверняка знаете, все логические и арифметические операции выполняются процессором. Процессоры могут иметь одно или несколько ядер. Одно ядро может выполнять только один набор инструкций в единицу времени. Соответственно, одноядерные процессоры не позволяют выполнять множество задач одновременно.

Однако даже на одноядерных CPU мы без проблем сможем писать код, слушать музыку и скачивать файлы по сети. И при этом нам будет казаться, что все выполняется одновременно. А достигается это с помощью конкурентности.

Конкурентность - это возможность разбивать алгоритм или компьютерную программу на отдельные блоки, которые могут выполняться независимо и коммуницировать между собой.

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

Если сформулировать проще, то можно сказать что конкурентность - это способность компьютера справляться с множеством задач одновременно.
Конкурентность - это способность компьютера справляться с множеством задач одновременно.
Давайте вернемся к нашему примеру. Представим что вы пишите код и слушаете музыку, работая на одноядерном процессоре. На уровне операционной системы, эти задачи разбиваются на отдельные блоки, которые выполняются процессором по очереди, а переключение между этими блоками происходит настолько быстро, что создается впечатление, будь-то эти процессы выполняются одновременно. Однако это не так.

За приоритизацию и переключение между данными задачами отвечает планировщик ОС. В данном видео я не буду опускаться в детали реализации данной логики на уровень ОС и CPU. Для того чтобы понять данные концепции это не столь важно. Если же вам интересно то вы можете углубиться в эту тему самостоятельно.

Параллелизм
Многоядерный процессор способен обрабатывать несколько инструкций в единицу времени, по одной инструкции на ядро. Соответственно, многоядерные процессоры умеют выполнять несколько задач одновременно, за счет чего достигается параллелизм.

Из этого выплывает что параллелизм - способность компьютера выполнять несколько задач одновременно.
Как видите, разница в этих понятиях не столь очевидна.

Конкурентность - это способность компьютера справляться с множеством задач одновременно, в то время как параллелизм - способность компьютера выполнять несколько задач одновременно.

Конкурентность - это про то, как мы структурируем логику выполнения программы, как мы делим ее на логические блоки, которые могут выполняться параллельно или поочередно.

Конкурентность - это в первую очередь дизайн подход к проектированию ПО, в то время как параллелизм - это способ его выполнения.

Конкурентный код может выполняться как параллельно, на нескольких ядрах процессора, так и на одном ядре, в последовательной манере.

Поэтому конкурентность не равно параллелизм. Это два разных понятия, и их не нужно путать.
Конкурентность - это в первую очередь дизайн подход к проектированию ПО, в то время как параллелизм - это способ его выполнения.
Чтобы еще лучше понять разницу, давайте возьмем пример из реальной жизни.

Допустим, вы утром собираетесь на работу. Ваша обычная утренняя рутина состоит из того чтобы принять душ, почистить зубы, одеться, выпить кофе и, собственно, поехать на работу.

Вы можете сделать данную серию действий в разный способ.

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

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

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

А еще вы можете вообще не варить кофе, а купить его в кофейне по дороге на работу.

Эти варианты - это уже конкурентное решение задачи. Вместо того, чтобы выполнять задачи один за одним, вы можете переключаться между ними, чтобы ускорить весь процесс. Однако в любом случае, вы один занимаетесь решением всех задач.

Если же вы, например, живете со своей второй половинкой, то можете попросить ее (или его) сварить вам кофе пока вы принимаете душ и чистите зубы. Такой подход уже будет параллельным, поскольку одну из задач, а именно приготовление кофе, вы отдали другому человеку.

Окей, а как это работает на практике?
Когда вы запускаете приложение, на уровне операционной системы создается новый процесс, или несколько процессов.

На маке можно открыть приложение Activity Monitor, и посмотреть список всех процессов. Также, здесь можно увидеть общее количество запущенных процессов и потоков.
На Windows для этого можно воспользоваться командой tasklist в командной строке, а в линуксе выполнить команду ps или top в терминале.

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

На уровне самих процессов, конкурентность достигается за счет многопоточности. Каждый процесс имеет как минимум один главный поток, или Thread на английском. Поток можно воспринимать как легковесный процесс внутри самого процесса.

В качестве реального примера, давайте возьмем веб-сервер. Его главная задача - обрабатывать входящие запросы одновременно, с максимально быстрой скоростью. Веб-серверы могут принимать десятки, сотни, тысячи и даже сотни тысяч запросов в секунду. Для этого они обрабатывают каждый запрос конкурентно, в отдельном потоке. Сервер может создавать новые потоки под запросы, или брать уже аллоцированные в памяти потоки и общего пулла потоков.

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

Такая ситуация называется гонкой данных. В своих видео на YouTube я разберу реализацию конкурентности в языке Go и рассмотрю это явление более детально.

Я надеюсь, после прочтения этой статьи вам понятна разница между данными двумя концепциями.

А если вы хотите более подробно разобраться в данной теме, рекомендую ознакомиться со следующими материалами:

- Доклад Роба Пайка "Concurrency is not Parallelism"
- Видео от канала "Диджитализируй", в котором Алексей очень понятным способом объясняет конкурентность и параллелизм.


Спасибо что дочитали до конца, чести и удачи!