Прежде чем приступать к раскрытию темы нашей статьи, неплохо бы прояснить, что же такое параллелизм и конкурентность и в чём разница между ними. Golang — это тот язык, который делает возможной работу приложения в условиях конкурентности и параллелизма.
Давайте разберёмся, чем они друг от друга отличаются.
Конкурентность предполагает работу приложения с двумя и более задачами одновременно, когда происходит создание нескольких процессов, выполняющихся независимо друг от друга.
Приложения могут иметь дело с большим количеством процессов сразу для достижения желаемого поведения. Допустим, есть простенький интернет-магазин. Посмотрим, какие могут быть одновременно выполняемые задачи. Вот их список:
Для интернет-магазина важно, чтобы все эти задачи выполнялись одновременно, ведь нужно удержать пользователей на сайте или в приложении, сделав его максимально привлекательным для них, чтобы они оставили здесь свои деньги. Поэтому можно сделать так, чтобы на простом сайте в фоновом режиме выполнялось множество задач.
На картинке выше у нас несколько задач, выполняемых одновременно, но есть разница в том, как они выполняются. Рассмотрим теперь подробнее.
Допустим, у нас одноядерная система и надо выполнить несколько задач, но есть ограничение: одномоментно может быть выполнена лишь одна задача.
В модели конкурентного выполнения имеет место переключение контекста между задачами: приложение работает с несколькими задачами, но не может выполнять их все вместе, ведь ядро всего одно. Переключение контекста происходит настолько быстро, что создаётся ощущение, что задачи выполняются одновременно.
Фактор параллельного выполнения здесь отсутствует: параллельные процессы не могут выполняться вместе просто потому, что наша система одноядерная.
На второй картинке в нижней части проиллюстрирована конкурентность без параллелизма. Здесь показано конкурентное выполнение двух задач с переключением контекста: одномоментно может быть выполнена лишь одна задача.
Добавим приложению параллелизмаВ случае с одноядерной системой у нас были ограничения по ресурсам. Если мы добавим несколько ядер, ресурсов станет больше и приложение сможет одновременно выполнять на разных ядрах множество задач. В верхней части той же картинки показано, как на разных ядрах одновременно и параллельно выполняются две задачи.
Конкурентность и параллелизм — очень похожие понятия, но мне кажется, что разницу вы уже уловили.
Таким образом, увеличивая сложность системы, можно увеличить и сложность решаемых с её помощью задач: работая с Golang, мы можем масштабировать приложение, с лёгкостью переходя от конкурентного исполнения к параллельному. Масштабируемость в Golang — это легко!
Прежде чем разбирать конкурентность и параллелизм в Golang, первым делом нужно понять, что из себя представляют горутины. Горутины реализуют в Golang обёрточный функционал потоков, а управляются они скорее из среды выполнения Go, нежели из операционной системы.
Среда выполнения Go распределяет или забирает ресурсы памяти у горутин. Горутина во многом похожа на поток тем, что касается выполнения множества задач, но потребляет меньше ресурсов, чем потоки операционной системы. Горутина не имеет полного соответствия с потоками.
Мы можем разделить приложение на множество конкурентных задач, которые могут выполняться с помощью различных горутин. Это предоставит возможность использовать конкурентности в приложении.
Если приложение выполняется на нескольких ядрах, то добавляется и параллелизм.
Преимущества горутин:
Теперь обратимся к простой программе на Golang:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
elapsedTime := time.Since(start)
fmt.Println("Total Time For Execution: " + elapsedTime.String())
time.Sleep(time.Second)
}
Этот код последовательно исполняет внутри основной функции Golang две функции, которые вызываются немедленно.
Здесь мы не используем горутины, а программа выполняется в том же потоке. Никакой конкурентности в приложение мы не добавили. При выполнении получаем такой вывод:
Эта программа выполняется последовательно, начиная с основного потока, после выполняется первая функция немедленного вызова, затем вторая, и потом завершается после выполнения всего, что осталось в теле функции.
В этом коде не было никакого конкурентного исполнения. Можете попробовать проделать нечто подобное в виртуальном редакторе:
<figure><iframe width="700" height="525" src="/media/c7575c02a42975093806da924e66199a" allowfullscreen=""></iframe></figure>
В вышеприведённом сценарии мы не добавляли никаких горутин к основной функции. Можем добавить горутину к программе ключевым словом go
перед выполнением функции.
Поставив ключевое слово go
перед функцией немедленного выполнения, мы добавляем конкурентность выполнению. Давайте посмотрим, как влияет это добавление ключевого слова go
на выполнение:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
go func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
go func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
elapsedTime := time.Since(start)
fmt.Println("Total Time For Execution: " + elapsedTime.String())
time.Sleep(time.Second)
}
Вывод:
А в этом сценарии мы добавляем ключевое слово go
к функциям немедленного выполнения. Выполнение начинается с функции main
.
Как только доходим до ключевого слова go
, создаётся отдельная горутина, добавляющая к приложению другой поток Go, отвечающий за выполнение функции на отдельном конкурентном потоке.
Аналогично будет создана следующая горутина, как только встретится второе ключевое слово go
. Она затем выполняет функцию немедленного вызова внутри другого потока горутины.
В данном сценарии в конкурентном режиме будут выполняться три потока: основной main
, поток первой функции немедленного выполнения first
и поток второй такой функции.
Попробуйте выполнить это в виртуальном редакторе ниже:
В приведённом выше коде мы добавили ключевое слово go
перед выполнением функции. При этом для выполнения функции создаётся отдельная горутина и эта функция выполняется внутри отдельного потока горутины.
В результате добавления go
перед функциями всякий раз выполнение происходило не в том же потоке, а создавался отдельный поток, что приводило к увеличению конкурентности и росту производительности.
В Go мы можем увеличить количество ядер простой строчкой кода. Приложению будет дана команда перейти на несколько ядер:
runtime.GOMAXPROCS(4)Здесь мы указали, что приложение может использовать четыре ядра для исполнения.
Создаваемые нами горутины могут выполняться вместе на разных ядрах, задействуя параллельное выполнение и ускоряя приложение.
package main
import (
"fmt"
"time"
"runtime"
)
func main() {
runtime.GOMAXPROCS(4)
start := time.Now()
go func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
go func() {
for i:=0; i < 3; i++ {
fmt.Println(i)
}
}()
elapsedTime := time.Since(start)
fmt.Println("Total Time For Execution: " + elapsedTime.String())
time.Sleep(time.Second)
}
Теперь программа сможет выполняться на нескольких ядрах параллельно и делать это быстрее. Исполнение кода будет вот таким:
С помощью этого GOMAXPROCS
мы запрашиваем переход приложения на несколько ядер. И ключевые слова go
, добавляющиеся перед исполнением функции, могут исполняться уже отдельно на разных ядрах, увеличивая производительность приложения.
Тут-то мы добавляем вместе с конкурентностью и параллелизм. Можете попробовать выполнить программу в данном виртуальном редакторе:
<figure><iframe width="700" height="525" src="/media/f09b3eba247e0a92e1eb190e4265bb47" allowfullscreen=""></iframe></figure>
Масштабировать приложение, переходя от режима конкурентного исполнения к параллельному, можно очень легко, если работать в Golang: просто присоединяем к функции ключевое слово go
и быстро увеличиваем сложность и скорость выполняемых в приложении задач.
Благодарю за внимание.
Перевод статьи Mayank Gupta: Understanding Golang and Goroutines
Комментарии