В этой статье обсуждается проблема синхронизации в параллелизме и конкуренции. А так же объясняется разница между этими понятиями. И как это реализовано в go. Без технических подробностей. Просто и понятно. Для людей.
Введение
Прежде чем продолжить рассказывать про параллельные и конкурентные приложения, думаю следует внести ясность в понятия "параллельные" и "конкурентные". С термином "параллельные" русскоязычному читателю вроде все понятно, а вот с термином "конкурентные" возникают ассоциации, скорее связанные с экономикой, чем с программированием. Было бы более привычно и понятно говорить "действующие одновременно", но так сложилось, что говорят именно "конкурентные". Так что, если вам будет более комфортно, держите в уме, что "конкурентные" - это "действующие одновременно". Как известно, Go имеет поддержку взаимодействия между приложениями и поддержку конкурентных приложений. Это программы, которые одновременно выполняют разные фрагменты кода, возможно, на разных процессорах или компьютерах. Основными строительными блоками для структурирования конкурентных программ являются горутины и каналы.
Что такое горутины?
Приложение представляет собой процесс, выполняемый на компьютере; процесс - это независимо выполняющийся объект, который работает в собственном адресном пространстве в памяти. Процесс состоит из одного или нескольких потоков операционной системы, которые выполняются одновременно в одном адресном пространстве. Почти все реальные программы являются многопоточными, например, чтобы иметь возможность обслуживать множество запросов одновременно (например, веб-серверы), или для увеличения пропускной способности или производительности (например, параллельное выполнение кода для разных наборов данных). Такое конкурентное приложение может выполняться на одном процессоре или ядре с использованием нескольких потоков операционной системы. Тем не менее, только тогда, когда один и тот же процесс приложения выполняется в один момент времени на нескольких ядрах или процессорах, он действительно называется параллельным.
Параллелизм - это способность ускорить работу за счет одновременного использования нескольких процессоров. Таким образом конкурентные программы могут быть, а могут и не быть параллельными.
Многопоточные приложения, как известно, сложно сделать правильно. Основная проблема заключается общих данных в памяти, которыми можно манипулировать разными потоками непредсказуемым образом, тем самым иногда получая невоспроизводимые и случайные результаты. Это называется состоянием(условием) гонки.
Примечание
Не используйте глобальные переменные или разделяемую память, потому что они делают ваш код небезопасным для одновременной работы.
Решение этой проблемы (проблемы гонки) заключается в синхронизации разных потоков и блокировке данных, чтобы только один поток, в момент времени, мог изменять данные. Однако опыт разработки программного обеспечения показал, что это приводит к сложному, подверженному ошибкам программированию, и снижению производительности. Этот классический подход явно не подходит для современного многоядерного и многопроцессорного программирования. Модель поток на соединение недостаточно эффективна.
Go придерживается другой, и во многих случаях, более подходящей парадигмы, известной как Коммуникационные Последовательные Процессы (Communicating Sequential Processes, CSP изобрел Чарльз Энтони Ричард Хоар)
Об изобретателе
Чарльз Энтони Ричард Хоар - английский учёный, специализирующийся в области информатики и вычислительной техники. Наиболее известен как разработчик алгоритма «быстрой сортировки», на сегодняшний день являющегося наиболее популярным алгоритмом сортировки.
Она так же известна как модель передачи сообщений. Применяется еще в Erlang.
Кусочки(части) приложения, которые работают одновременно, в Go называются горутинами. Нет однозначного соответствия между горутиной и потоком операционной системы: горутина выполняется одним или несколькими потоками в зависимости от их доступности.
Горутины работают в одном адресном пространстве. Следовательно, доступ к общей памяти должен быть синхронизирован. Это можно сделать через пакет sync
, но это крайне не рекомендуется. Вместо этого Go использует каналы для синхронизации горутин.
Когда системный вызов блокирует горутину (например, ожидает ввода-вывода), другие горутины продолжают выполняться в других потоках. Дизайн горутин скрывает многие сложности создания потоков и управления ими. Горутины легкие, намного легче потоков. Они используют мало памяти и ресурсов. Поскольку они дешевы в создании, большее количество из них при необходимости можно запускать "на лету"(порядка сотен тысяч в том же адресном пространстве). Количество горутин ограничивает объем доступной памяти. Кроме того, они используют сегментированный стек.
Сегментированный стек
Процесс имеет код, данные, кучу и сегменты стека. Сегменты данных и кучи совместно используются всеми потоками. А как на счет области стека? Область стека процесса делится между потоками, то есть, если есть 3 потока, то область стека процесса делится на 3 части, и каждая отводится 3 потокам. Другими словами, когда мы говорим, что каждый поток имеет свой собственный стек, этот стек фактически является частью области стека процесса, выделенной каждому потоку. Когда поток завершает свое выполнение, стек потока освобождается процессом. Таким образом, когда дело доходит до совместного использования, код, данные и области кучи являются общими, а область стека просто делится между потоками.
Управление стеком осуществляется автоматически. Сборщик мусора не управляет стеками. Вместо этого они освобождаются сразу после выхода горутины.
Горутины могут выполняться в нескольких потоках операционной системы. Но, что более важно, они также могут выполняться в потоках, позволяя обрабатывать множество задач с относительно небольшим объемом памяти. Горутины имеют временной интервал в потоках ОС, поэтому некоторое количество горутин может обслуживаться меньшим числом потоков ОС.
Существуют два стиля параллелизма: детерминированный (четко определенный порядок) и недетерминированный (блокировка / взаимное исключение, но неопределенный порядок). Горутины и каналы Go продвигают детерминированный параллелизм (например, каналы с одним отправителем и одним получателем).
Горутина реализована как функция или метод (это также может быть анонимная или лямбда-функция) и вызывается с ключевым словом go
. Это запускает функцию, выполняющуюся одновременно с текущим вычислением, но в том же адресном пространстве и со своим собственным стеком. Например:
go sum(bigArray) //вычислить сумму в фоновом режиме
Стек горутины увеличивается и уменьшается по мере необходимости. Нет возможности переполнения стека, и программисту не нужно беспокоиться о размере стека. Когда горутина завершает свою работу, она завершается тихо, что означает, что функции, запустившей ее, ничего не возвращается. Функцию main()
так же можно рассматривать как горутину, хоть она и не запускается с go
.
Резюмирую
Go делает упор первую очередь не на параллелизме, потому что конкурентные программы могут быть, а могут и не быть параллельными. Однако оказывается, что чаще всего хорошо спроектированная конкурентная программа также имеет отличные параллельные возможности.
Вот и все о параллелизме и конкурентности. Я старался описать эти сущности максимально просто и понятно. Надеюсь у меня получилось.
