[Перевод] Изучение параллелизма в Go с иллюстрациями

Программа перевода самородков

Изучите параллелизм в Go с помощью иллюстраций

Вы, наверное, слышали о Го в самых разных его проявлениях. Он набирает популярность по разным причинам. Go быстрый, простой и имеет отличное сообщество. Модель параллелизма — один из самых захватывающих аспектов изучения языка. Примитивы параллелизма в Go делают создание параллельных многопоточных программ простым и увлекательным. Я представлю примитивы параллелизма Go с помощью иллюстраций, надеясь сделать эти концепции более понятными и полезными для дальнейшего изучения. Эта статья предназначена для людей, которые не знакомы с Go и хотят узнать о примитивах параллелизма Go: горутинах и каналах.

Однопоточные и многопоточные программы

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

Это первый пример, который мы настроили, программа майнинга. Функция в этом примере выполняется:Добыча полезных ископаемых,добыча полезных ископаемыхиплавка. В нашем случае карьеры и руды представлены в виде массива строк, и каждая функция их получает и возвращает «обработанный» массив строк. Для однопоточного приложения структура программы выглядит следующим образом.

Есть 3 основные функции. Одинискатель,Одиншахтериплавильный завод. В этой версии программы наши функции выполняются в одном потоке, одна за другой, и этот единственный поток (суслик по имени Гэри) должен выполнять всю работу.

func main() {
 theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
 foundOre := finder(theMine)
 minedOre := miner(foundOre)
 smelter(minedOre)
}

Распечатав обработанный массив «ore» в конце каждой функции, мы получим следующий вывод:

From Finder: [ore ore ore]

From Miner: [minedOre minedOre minedOre]

From Smelter: [smeltedOre smeltedOre smeltedOre]

Этот стиль программирования имеет то преимущество, что его легко проектировать, но что происходит, когда вы хотите использовать преимущества нескольких потоков и выполнять функции независимо друг от друга? Здесь в игру вступает параллельное программирование.

Эта схема майнинга более эффективна. Теперь несколько потоков (гоферов) работают независимо, поэтому Гэри не должен делать все это целиком. Один суслик ищет руду, один добывает руду, а третий плавит руду — вероятно, все это одновременно.

Для того, чтобы мы могли внедрить этот тип функциональности в наш код, нам нужны две вещи: способ создания независимо работающих сусликов и способ для общения сусликов друг с другом (отправить руду) Методы. Вот тут-то и появляются примитивы параллелизма Go: сопрограммы и каналы Go.

Go сопрограммы

Горутины можно рассматривать как легкие потоки. Создать подпрограмму Go так же просто, как добавитьgoДобавляется в начало вызывающей функции. В качестве простого примера давайте создадим две функции майнинга, используяgoКлючевое слово вызывает их и выводит каждый раз, когда они находят «руду» в шахте.

func main() {
 theMine := [5]string{“rock”, “ore”, “ore”, “rock”, “ore”}
 go finder1(theMine)
 go finder2(theMine)
 <-time.After(time.Second * 5) //你可以先忽略这个
}

Вот вывод нашей программы:

Finder 1 found ore!
Finder 2 found ore!
Finder 1 found ore!
Finder 1 found ore!
Finder 2 found ore!
Finder 2 found ore!

Как видно из приведенного выше вывода, искатели работают одновременно. Не существует реального порядка того, кто первым нашел руду, и порядок не всегда одинаков при многократном запуске.

Это большой прогресс! Теперь у нас есть простой способ настроить многопоточную (multi-Gopher) программу, но что происходит, когда нам нужно, чтобы наши независимые сопрограммы Go взаимодействовали друг с другом? добро пожаловать в магиюрядМир.

ряд

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

myFirstChannel := make(chan string)

Go сопрограммы могут быть на каналахОтправитьиперенимать. Это делается с помощью стрелок (

myFirstChannel <- "hello" // 发送
myVariable := <- myFirstChannel // 接收

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

Я обновил пример, чтобы код майнинга и функция майнинга были установлены как анонимные функции. Если вы никогда не видели лямбда-функции, не зацикливайтесь на этой части программы, просто знайте, что каждая функция вызывается сgoключевое слово, поэтому они работают на своих собственных сопрограммах. Важно отметить, как сопрограммы Go используют каналы.oreChanпередавать данные между собой.Не волнуйтесь, я объясню анонимные функции в конце.

func main() {
 theMine := [5]string{“ore1”, “ore2”, “ore3”}
 oreChan := make(chan string)

 // 寻矿者
 go func(mine [5]string) {
  for _, item := range mine {
   oreChan <- item //send
  }
 }(theMine)

 // 矿工
 go func() {
  for i := 0; i < 3; i++ {
   foundOre := <-oreChan //接收
   fmt.Println(“Miner: Received “ + foundOre + “ from finder”)
  }
 }()
 <-time.After(time.Second * 5) // 还是先忽略这个
}

В выводе ниже вы можете видеть, что наш майнер считывает канал руды три раза, каждый раз получая кусок «руды».

Miner: Received ore1 from finder

Miner: Received ore2 from finder

Miner: Received ore3 from finder

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

блокировка канала

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

отправить блокировку

Как только горутина (гофер) отправляет сообщение по каналу, отправляющая горутина блокируется до тех пор, пока другая горутина не получит сообщение, отправленное каналом.

получить блокировку

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

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

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

небуферизованный канал

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

буферный канал

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

bufferedChan := make(chan string, 3)

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

bufferedChan := make(chan string, 3)

go func() {
 bufferedChan <- "first"
 fmt.Println("Sent 1st")
 bufferedChan <- "second"
 fmt.Println("Sent 2nd")
 bufferedChan <- "third"
 fmt.Println("Sent 3rd")
}()

<-time.After(time.Second * 1)

go func() {
 firstRead := <- bufferedChan
 fmt.Println("Receiving..")
 fmt.Println(firstRead)
 secondRead := <- bufferedChan
 fmt.Println(secondRead)
 thirdRead := <- bufferedChan
 fmt.Println(thirdRead)
}()

Порядок печати между нашими двумя подпрограммами Go следующий:

Sent 1st
Sent 2nd
Sent 3rd
Receiving..
first
second
third

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

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

объединить их

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

theMine := [5]string{"rock", "ore", "ore", "rock", "ore"}
oreChannel := make(chan string)
minedOreChan := make(chan string)
// Finder
go func(mine [5]string) {
 for _, item := range mine {
  if item == "ore" {
   oreChannel <- item //在 oreChannel 上发送东西
  }
 }
}(theMine)
// Ore Breaker
go func() {
 for i := 0; i < 3; i++ {
  foundOre := <-oreChannel //从 oreChannel 上读取
  fmt.Println("From Finder: ", foundOre)
  minedOreChan <- "minedOre" //向 minedOreChan 发送
 }
}()
// Smelter
go func() {
 for i := 0; i < 3; i++ {
  minedOre := <-minedOreChan //从 minedOreChan 读取
  fmt.Println("From Miner: ", minedOre)
  fmt.Println("From Smelter: Ore is smelted")
 }
}()
<-time.After(time.Second * 5) // 还是一样,你可以忽略这些

Вывод программы следующий:

From Finder:  ore

From Finder:  ore

From Miner:  minedOre

From Smelter: Ore is smelted

From Miner:  minedOre

From Smelter: Ore is smelted

From Finder:  ore

From Miner:  minedOre

From Smelter: Ore is smelted

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

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

Прежде чем идти, вы должны знать...

Анонимные процедуры Go

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

// 匿名 Go 协程
go func() {
 fmt.Println("I'm running in my own go routine")
}()

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

Основная функция — подпрограмма Go.

Основная программа на самом деле работает в своей собственной процедуре Go! Более важно знать, что как только основная функция вернется, она закроет все другие запущенные сопрограммы. Вот почему у нас есть таймер внизу основной функции — он создает канал и отправляет значение через 5 секунд.

<-time.After(time.Second * 5) //在 5 秒后从通道接收

Помните, как подпрограмма Go блокирует чтение до тех пор, пока что-то не будет отправлено? Если добавить приведенный выше код, это именно то, что происходит с основной сопрограммой. Основная сопрограмма будет заблокирована, что даст нашим другим сопрограммам дополнительные 5 секунд жизни для выполнения.

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

func main() {
 doneChan := make(chan string)
 go func() {
  // Do some work…
  doneChan <- “I’m all done!”
 }()
 
 <-doneChan // 阻塞直到 Go 协程发出工作完成的信号
}

Вы можете ранжировать значения на канале

В предыдущем примере мы заставили наш майнер читать канал через 3 итерации в цикле for. Что произойдет, если мы не знаем точно, сколько руды отправит искатель? Затем, подобно выборке диапазона для коллекций, вы можетеДиапазон значений на канале.

Обновляя нашу предыдущую функцию майнера, мы можем написать:

 // 矿工
 go func() {
  for foundOre := range oreChan {
   fmt.Println(“Miner: Received “ + foundOre + “ from finder”)
  }
 }()

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

Примечание. Ранжированные значения на канале будут блокировать канал до тех пор, пока по каналу не будет отправлен другой пакет. Единственный способ предотвратить блокировку сопрограммы Go после того, как все отправки произошли, — это закрыть канал «close (channel)».

Вы можете делать неблокирующие чтения на канале

Но все, что вы только что сказали нам, это то, как каналы могут блокировать горутины? ! Верно, но есть способ использовать Goselect caseСтруктуры выполняют неблокирующие операции чтения в канале. Используя приведенную ниже структуру, ваша сопрограмма Go будет читать из канала, если что-то есть, в противном случае запускать по умолчанию.

myChan := make(chan string)
 
go func(){
 myChan <- “Message!”
}()
 
select {
 case msg := <- myChan:
  fmt.Println(msg)
 default:
  fmt.Println(“No Msg”)
}
<-time.After(time.Second * 1)
select {
 case msg := <- myChan:
  fmt.Println(msg)
 default:
  fmt.Println(“No Msg”)
}

При запуске этот пример имеет следующий вывод:

No Msg  
Message!

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

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

select {  
 case myChan <- “message”:  
  fmt.Println(“sent the message”)  
 default:  
  fmt.Println(“no message sent”)  
}

следующие шаги

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

Google I/O 2012 — Перейти в режим параллелизма

Роб Пайк —  «Параллелизм — это не параллелизм».

GopherCon 2017: Эдвард Мюллер — Go Anti-pattern

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

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


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.