Примечание: я настоятельно рекомендую посмотреть 'Concurrency Is Not Parallelism', выступление Роба Пайка (@rob_pike) в 2012 году. Роб не только эксперт по Go, он также отлично разбирается в симпатичных аналогиях с сусликом. / em>

Параллелизм - это то же самое, что параллелизм?

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

Параллелизм

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

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

На очень высоком уровне параллелизм можно представить так:

Параллелизм

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

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

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

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

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

Как мы используем параллелизм в Go? С помощью горутин. Хорошо ... что такое горутина? Горутины можно рассматривать как легкие и недорогие потоки, управляемые средой выполнения Go. Однако важно отметить, что несколько горутин не выполняются каждая в отдельном потоке или процессе. На самом деле в одном потоке может быть много горутин. Среда выполнения Go динамически мультиплексирует горутины в потоки по мере необходимости, чтобы каждая горутина продолжала работать. Как удобно!

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

func greeting(s string) {
  fmt.Printf("Hello, %s", s)
}
go greeting("Elizabeth")
// logs "Hello, Elizabeth"

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

Давайте посмотрим на следующую программу:

package main
import (
  "fmt"
  "time"
)
func numSearch(targetVal int) {
  searchList := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  time.Sleep((time.Duration(targetVal) * 100) * time.Millisecond)
  for _, num := range searchList {
    if num == targetVal {
      fmt.Printf("I found the targetVal: %d \n", targetVal)
      return
    }
  }
  fmt.Println("targetVal not found :(")
}
func main() {
  numSearch(10)
  numSearch(1)
  // time.Sleep(10000 * time.Millisecond)
  fmt.Println("The program has finished executing.")
}

Когда вы запустите приведенный выше код, вы ясно увидите, что он выполняется последовательно. Из-за вызова time.Sleep() в numSearch() функция numSearch() всегда будет выполняться дольше, когда передается аргумент 10, чем когда мы передаем 1, но поскольку вызовы функции numSearch() выполняются последовательно, второй вызов не запускается до тех пор, пока первый вызов завершен. Результат будет выглядеть так:

I found the targetVal: 10
I found the targetVal: 1
The program has finished executing.

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

package main
import (
  "fmt"
  "time"
)
func numSearch(targetVal int) {
  searchList := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
  time.Sleep((time.Duration(targetVal) * 100) * time.Millisecond)
  for _, num := range searchList {
    if num == targetVal {
      fmt.Printf("I found the targetVal: %d \n", targetVal)
      return
    }
  }
  fmt.Println("targetVal not found :(")
}
func main() {
  go numSearch(10)
  go numSearch(1)
  time.Sleep(10000 * time.Millisecond)
  fmt.Println("The program has finished executing.")
}

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

I found the targetVal: 1
I found the targetVal: 10
The program has finished executing.

Вы можете видеть, что второй вызов numSearch() не дожидался завершения numSearch(10). Это потому, что вызовы функций выполнялись как горутины и выполнялись одновременно. numSearch(1) завершено до numSearch(10), поэтому мы видим результат выше.

Использование параллелизма

Итак, приведенный выше пример немного демонстрирует параллелизм, но он не совсем демонстрирует реальные преимущества.

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

А как насчет параллелизма ..?

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

Подведение итогов

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

Изменить: следующая статья Go: Concurrency + Channels уже опубликована!

А пока поиграйте с горутинами и получайте удовольствие, экспериментируя с мощью параллелизма!

Хотите стать участником Medium?

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

Другие источники