ГОЛАНГ

Достижение параллелизма в Go

Если бы мне пришлось выбрать одну замечательную функцию Go, то это была бы встроенная модель параллелизма. Он не только поддерживает параллелизм, но и делает его лучше. Перейти к модели параллелизма (горутины) для параллелизма - это то же самое, что Docker для виртуализации.

☛ Что такое параллелизм?

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

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

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

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

☛ Что такое параллелизм?

Но тогда возникает вопрос, а как насчет того, чтобы у моего процессора было несколько ядер? Если процессор имеет несколько процессоров, он называется многоядерным процессором. Возможно, вы слышали об этом термине, покупая ноутбук, компьютер или смартфон. Многоядерный процессор может обрабатывать сразу несколько задач.

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

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

☛ параллелизм против параллелизма

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

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

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

☛ Что такое компьютерный процесс?

Когда вы пишете компьютерную программу на таких языках, как C, java или Go, это просто текстовый файл. Но поскольку ваш компьютер понимает только двоичные инструкции, состоящие из 0s и 1s, вам необходимо скомпилировать этот код на машинный язык. Здесь на помощь приходит компилятор. В языках сценариев, таких как python и javascript, интерпретатор делает то же самое.

Когда скомпилированная программа отправляется в ОС для обработки, ОС выделяет разные вещи, такие как адресное пространство памяти (где будут располагаться куча и стеки процесса), счетчик программы, PID (идентификатор процесса ) и другие очень важные вещи. У процесса есть по крайней мере один поток, известный как основной поток, в то время как основной поток может создавать несколько других потоков. Когда основной поток завершает свое выполнение, процесс завершается.

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

☛ Что такое нить?

Поток - это легкий процесс внутри процесса. Поток - это фактический исполнитель фрагмента кода. Поток имеет доступ к памяти, предоставляемой процессом, ресурсам ОС и другим вещам.

Во время выполнения кода поток сохраняет переменные (данные) внутри области памяти, известной как stack который scratch space где переменные содержат временное пространство. Стек создается во время выполнения и обычно имеет фиксированный размер, предпочтительно 1-2 МБ. Хотя стек потока может использоваться только этим потоком и не будет использоваться другим потоком. Куча - это свойство процесса, доступное для использования любым потоком. Куча - это пространство общей памяти, в котором данные из одного потока могут быть доступны и другим потокам.

Теперь у нас есть общее представление о процессе и потоке. Но в чем их польза?

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

Ниже приведен снимок экрана приложения Chrome Browser на платформе macOS.

Выше снимок экрана показывает, что браузер Google Chrome использует разные процессы для открытых вкладок и внутренних служб. Поскольку у каждого процесса есть хотя бы один поток, мы можем видеть, что процесс Google Chrome в этом случае имеет более 10 потоков.

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

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

☛ Планирование потоков

Когда несколько потоков работают последовательно или параллельно, поскольку несколько потоков могут совместно использовать некоторые данные, потоки должны работать согласованно, чтобы только один поток мог получить доступ к определенным данным одновременно. Выполнение нескольких потоков в определенном порядке называется планированием. Потоки Os планируются ядром, а некоторые потоки управляются средой выполнения языка программирования, например JRE. Когда несколько потоков пытаются получить доступ к одним и тем же данным одновременно, что вызывает изменение данных или приводит к неожиданному результату, происходит race condition.

При разработке параллельных программ Go нам нужно искать условия гонки, о которых мы поговорим в следующих уроках.

☛ Параллелизм в Go

Наконец, мы подошли к моменту, когда поговорим о том, как Go реализует параллелизм. В традиционных языках, таких как java, есть класс потока, который можно использовать для создания нескольких потоков в текущем процессе. Поскольку Go не имеет традиционного синтаксиса ООП, он предоставляет ключевое слово go для создания goroutines. Когда ключевое слово go помещается перед вызовом функции, оно становится goroutines.

Мы поговорим о горутинах в следующем уроке, но в двух словах, горутины ведут себя как потоки, но технически; это абстракция над потоками.

Когда мы запускаем программу Go, среда выполнения создает несколько потоков в ядре, в которых все горутины мультиплексируются (порождены). В любой момент времени один поток будет выполнять одну горутину, и если эта горутина заблокирована, она будет заменена другой горутиной, которая будет выполняться в этом потоке. Это похоже на планирование потоков, но обрабатывается средой выполнения Go, и это намного быстрее.

В большинстве случаев рекомендуется запускать все ваши горутины на одном ядре, но если вам нужно разделить горутины между доступными ядрами ЦП вашей системы, вы можете использовать переменную среды GOMAXPROCS или вызвать среду выполнения с помощью функции runtime.GOMAXPROCS(n), где n - число ядер для использования. Но иногда вы можете почувствовать, что настройка GOMAXPROCS > 1 замедляет работу вашей программы. Это действительно зависит от характера вашей программы, но вы можете найти решение или объяснение своей проблемы в Интернете. На практике программы, которые тратят больше времени на общение по каналам, чем на вычисления, будут испытывать снижение производительности при использовании нескольких ядер, потоков ОС и процессов.

Go имеет M:N планировщик, который также может использовать несколько процессоров. В любое время M горутины должны быть запланированы для N потоков ОС, которые выполняются не более чем на GOMAXPROCS процессорах. В любое время разрешено запускать не более одного потока на ядро. Но при необходимости планировщик может создать больше потоков, но это случается редко. Если ваша программа не запускает никаких дополнительных горутин, она, естественно, будет работать только в одном потоке, независимо от того, сколько ядер вы разрешите ей использовать.

☛ темы против горутин

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

Выше приведены несколько важных отличий, но если вы погрузитесь глубже, вы откроете для себя удивительный мир модели параллелизма Go. Чтобы выделить некоторые сильные стороны силы параллелизма Go, представьте, что у вас есть веб-сервер, на котором вы обрабатываете 1000 запросов в минуту. Если вам приходилось запускать каждый запрос одновременно, это означает, что вам нужно создать 1000 потоков или разделить их по разным процессам. Так сервер Apache управляет входящими запросами (читайте здесь). Если поток ОС использует размер стека 1 МБ на поток, это означает, что вы исчерпаете 1 ГБ ОЗУ для этого трафика. Apache предоставляет директиву ThreadStackSize для управления размером стека для каждого потока, но вы не знаете, столкнетесь ли вы с проблемой из-за этого.

В случае горутин, поскольку размер стека может расти динамически, вы можете без проблем создать 1000 горутин. Поскольку горутина начинается с 8 КБ (2 КБ начиная с Go 1.4) стекового пространства, большинство из них обычно не становятся больше этого. Но если есть рекурсивная операция, требующая больше памяти, Go может увеличить размер стека до 1 ГБ, что, как я думаю, вряд ли когда-либо произойдет, за исключением for {}, что, очевидно, является ошибкой.

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

  • сетевой ввод
  • спать
  • работа канала
  • блокировка примитивов в пакете синхронизации

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

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

Дополнительные ресурсы

Есть отличная статья о планировщике Go под названием Планировщик Go's work-stealing от Яаны Доган, которую вы должны прочитать, чтобы узнать, как среда выполнения Go управляет горутинами.

Роб Пайк сделал отличный доклад о параллелизме в GoLang под названием Параллелизм - это не параллелизм.

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