Горутины: как запустить код параллелизма в Go

Горутины: как запустить код параллелизма в Go

5 апреля 2022 г.

Одной из самых сильных сторон языка программирования Go является его встроенная поддержка параллелизма, основанная на книге Тони Хоара «Общение последовательных процессов». Как разработчик с опытом работы с JS и Java, я был удивлен тем, насколько легко вы можете запускать параллельный код в Go.


Разница между параллелизмом в Go и других языках


На самом деле, горутины — это не поток. Это зеленые нити. Давайте посмотрим, что такое зеленая нить.


В компьютерном программировании зеленые потоки или виртуальные потоки – это потоки , запланированные библиотекой времени выполнения или виртуальной машиной. ) (ВМ), а не изначально базовой операционной системой (ОС).


- Википедия


Зеленые потоки эмулируют многопоточные среды, не полагаясь на какие-либо собственные возможности ОС, и они управляются в пользовательском пространстве, а не ядре , что позволяет им работать в средах, не поддерживающих собственные потоки. Планировщик Go отвечает за переключение потоков между горутинами. В результате переключение контекста между зелеными потоками (в нашем случае горутинами) эффективно дешевле, чем потоки ОС. Начальный размер стека горутины составляет 2 КБ (и может быть уменьшен) в отличие от [\~8 МБ] (https://unix.stackexchange.com/questions/127602/default-stack-size-for-pthreads) стека. потока ОС.


Подводя итог, go scheduler работает внутри go runtime внутри пользовательского пространства и с использованием потоков ОС. Горутины выполняются в контексте потоков ОС.



Совместный и упреждающий


До версии 1.14 в Go было только совместное планирование. Это означает, что горутина сама решает, когда освобождать ресурсы по любой причине (например, вызов функции, любые операции ввода-вывода, ожидание мьютекса, чтение из канала и т. д.). И это может вызвать проблему загрузки ЦП одной горутиной и не достигает ни одной из вышеперечисленных причин. Итак, в версии 1.14 было введено асинхронное вытеснение. Асинхронное вытеснение запускается на основе временного условия. Когда горутина выполняется более 10 секунд, планировщик Go попытается ее вытеснить.


Давайте посмотрим, как это работает. Прежде всего, как создать горутину?


Чтобы создать горутину, нам нужно использовать ключевое слово «go» следующим образом:


```иди


иди функ () {


//логика параллельной функции


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


Наш полный пример будет выглядеть так:


```иди


основной пакет


импорт (


"ФМТ"


"время выполнения"


основная функция () {


время выполнения.GOMAXPROCS(1)


я: = 0


go func(i *int) {


для {


*я++


}(&я)


время выполнения.Gosched()


fmt.Println(i)


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


Каналы


Иногда нам нужен способ общения между горутинами. В Go есть специальный слоган:


Не общайтесь, делясь памятью; вместо этого делитесь памятью, общаясь.


Что это значит? Работа с параллельными программами всегда совсем не проста, потому что вы всегда должны помнить об условиях гонки, взаимоблокировках и других проблемах. Go представляет каналы для решения этой проблемы. Канал — это тип связи между горутинами. Он имеет тип (int, string, некоторая структура) и создается ключевым словом make.


```иди


сделать (ch chan int)


Для записи или чтения чего-либо из канала существует специальный синтаксис:


```иди


ch <- 2 // запись


v := <- ch // чтение и присвоение результата переменной v


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


Вы также можете перебирать канал.


```иди


для v := диапазон ch {


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


Вы также можете закрыть канал, и в результате цикл for остановит итерацию по закрытому каналу.


```иди


закрыть (ч)


Поисковый робот


В качестве примера создадим простую функцию, которая будет проверять статус сайта


```иди


основной пакет


импорт (


"ФМТ"


"сеть/http"


основная функция () {


веб-сайты := []строка{


"https://hackernoon.com/",


"https://github.com/",


"https://apple.com/",


"https://google.com/",


"https://youtube.com/",


"https://www.udemy.com/",


"https://netflix.com/",


"https://www.coursera.org/",


"https://facebook.com/",


"https://microsoft.com",


"https://wikipedia.org",


"https://educative.io",


"https://acloudguru.com",


для _, веб-сайт := диапазон веб-сайтов {


checkResource (веб-сайт)


func checkResource (строка веб-сайта) {


если разрешение, ошибка := http.Get(веб-сайт); ошибка != ноль {


fmt.Println(веб-сайт, "не работает")


} еще {


fmt.Printf("[%d] %s работает
", res.StatusCode, веб-сайт)


Если вы запустите это, вы увидите такие журналы в консоли:


:::Информация


[200] https://hackernoon.com/ работает [200] https://github.com/ работает [200] https://apple.com/ вверх [200] https://google.com/ выросло [200] https://youtube.com/ выросло [200] https://www.udemy.com / до [200] https://netflix.com/ до [200] https://www.coursera.org/ до [200] https:// facebook.com/ вырос [200] https://microsoft.com вырос [200] https://wikipedia.org вырос [200] https://educative .io вырос [200] https://acloudguru.com вырос


Вызов этого кода займет около 10 секунд. Проблема, конечно, в одновременной проверке каждого ресурса один за другим. Теперь давайте попробуем сделать это немного быстрее. Для этого мы будем использовать шаблон пула рабочих. Вы будете использовать пул горутин для управления параллельно выполняемой работой. Используя цикл for, вы создадите определенное количество рабочих горутин в качестве пула ресурсов. Затем в вашем «потоке» main() вы будете использовать канал для обеспечения работы.


Прежде всего, нам нужно определить работника для нашего случая. Это будет выглядеть так:


```иди


func worker(ресурсы, результаты chan string) {


для ресурса := диапазон ресурсов {


если разрешение, ошибка: = http.Get (ресурс); ошибка != ноль {


результаты <- ресурс + "не работает"


} еще {


результаты <- fmt.Sprintf("[%d] %s работает", res.StatusCode, ресурс)


Давайте быстро узнаем, что именно здесь происходит. Каждый работник будет ждать ресурса веб-сайта из канала «ресурсы», и сразу после того, как кто-то отправит URL-адрес ресурса в канал, рабочий получит этот URL-адрес, проверит, все ли в порядке, и отправит результат на другой канал с именем « результаты`.


Теперь давайте посмотрим, как мы будем запускать наш рабочий пул:


```иди


основная функция () {


веб-сайты := []строка{


ресурсы := make(строка chan, 6)


результаты: = make (строка chan)


для я := 0; я < 6; я++ {


иди рабочий(ресурсы,результаты)


Наш пул воркеров содержит 6 горутин, которые запущены и ждут ресурсов для проверки. Здесь мы можем использовать как отдельную горутину IIF или немедленно вызываемую функцию:


```иди


иди функ () {


для _, v := диапазон веб-сайтов {


ресурсы <- v


Почему бы нам не использовать здесь синхронный встроенный код? Когда вы можете попытаться взять окончательный пример, удаленный go, и вы поймаете [тупик] (https://en.wikipedia.org/wiki/Deadlock).


Теперь у нас есть не только наш пул воркеров, но и обеспечиваем их работой :) Так как последнее, что нам нужно сделать, это прочитать результаты из пула. Для этого мы можем перебрать канал results в основной горутине и вывести все результаты проверки каждого сайта:


```иди


для я := 0; я < len(веб-сайты); я++ {


fmt.Println(<-результаты)


Полный код будет выглядеть так:


```иди


основной пакет


импорт (


"ФМТ"


"сеть/http"


основная функция () {


веб-сайты := []строка{


"https://hackernoon.com/",


"https://github.com/",


"https://apple.com/",


"https://google.com/",


"https://youtube.com/",


"https://www.udemy.com/",


"https://netflix.com/",


"https://www.coursera.org/",


"https://facebook.com/",


"https://microsoft.com",


"https://wikipedia.org",


"https://educative.io",


"https://acloudguru.com",


ресурсы := make(строка chan, 6)


результаты: = make (строка chan)


для я := 0; я < 6; я++ {


иди рабочий(ресурсы,результаты)


иди функ () {


для _, v := диапазон веб-сайтов {


ресурсы <- v


для я := 0; я < len(веб-сайты); я++ {


fmt.Println(<-результаты)


func worker(ресурсы, результаты chan string) {


для ресурса := диапазон ресурсов {


если разрешение, ошибка: = http.Get (ресурс); ошибка != ноль {


результаты <- ресурс + "не работает"


} еще {


результаты <- fmt.Sprintf("[%d] %s работает", res.StatusCode, ресурс)


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



Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE