Мастер параллелизма в Go

GoLang имеет невероятную поддержку параллельных программ, и в этой статье мы увидим, как мы можем оптимизировать программу, которая обрабатывает файл CSV, для отправки SMS-уведомлений своим пользователям.

Если вы новичок в использовании GoLang и хотите лучше понять, как работает параллелизм, я бы порекомендовал сначала прочитать эту статью: Объяснение параллелизма в GoLang, горутинах и каналах.

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

Программа должна прочитать этот файл и отправить уведомление каждому пользователю и его друзьям.

Вы можете найти исходный код в этом репозитории Github для справки.

Хорошо, приступим. Сначала мы создадим модуль Go, файл main.go и пакет с именем CSV. Этот пакет будет иметь метод с именем ProcessFile, который будет вызываться из основного пакета. Давайте сначала создадим модуль Go.

go mod init github.com/GithubHandle/fileProcessing

Исходная структура папок выглядит следующим образом:

Как видите, есть еще файл students.csv, вы можете взять этот файл из репозитория. Файлы main.go и csv.go выглядят так:

package main
import "github.com/YairFernando67/fileProcessing/csv"
func main() {
  csv.ProcessFile()
}
package csv
func ProcessFile() {
}

Теперь давайте откроем файл и прочитаем его содержимое:

В приведенном выше коде мы сначала открываем файл csv с помощью пакета os. Затем файл передается другой функции с именем scanFile. Эта функция использует пакет bufio для инициализации нового сканера и сканирования каждой строки файла. Для каждой строки мы извлекаем информацию о пользователе и добавляем ее к срезу пользователей. После завершения сканирования функция возвращает срез пользователей вызывающей стороне.

Структура пользователя определена в csv/user.go и выглядит так.

package csv
type User struct {
  Id, Name, LastName, Email, Phone string
  FriendIds                 []string
}

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

Сладко, теперь у нас есть содержимое файла, представленное в виде среза пользователей, теперь мы можем использовать этот срез для отправки SMS-уведомления каждому из этих пользователей и их друзьям.

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

Для последовательной обработки мы создали функцию и передали в нее пользователей. Эта функция распространяется на пользователей и для каждого пользователя проверяет, был ли он уже посещен ранее, если он не был посещен, он помечает пользователя как посещенного и отправляет SMS-уведомление. Затем он также просматривает друзей пользователя ids, находит каждого пользователя и выполняет те же действия, проверяя, был ли он посещен или нет, помечая их как посещенных и отправляя уведомление.

В функции sendSmsNotification мы используем функцию time.Sleep для имитации некоторой задержки при отправке уведомления.

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

Бежим go test -bench=. github.com/yaairfernando/fileProcessing/csv -benchtime=5x

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

Для этой параллельной реализации мы создали два канала: usersCh будет содержать первоначальный список пользователей, а unvisitedUsers будет содержать отдельных непосещенных пользователей.

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

Затем мы звоним initializeWorkers. Эта функция по существу инициализирует N горутин, определяемых константой MAX_GOROUTINES, в данном случае мы начнем с 10. Каждый рабочий процесс — это функция, которая слушает канал unvisitedUsers и для каждого пользователя, которого она получает, отправляет SMS-уведомление на пользователя, а также обрабатывает его идентификаторы друзей, находя каждого пользователя в списке, а затем отправляя пользователей на канал usersCh. Обработка друга пользователя ids выполняется в отдельной горутине, так как мы не хотим блокировать и здесь текущую горутину.

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

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

Затем в строке 37 у нас есть функция processUsers, которая охватывает канал usersCh, и для каждого списка пользователей, которые она получает, она проверяет, был ли пользователь уже посещен, если нет, то помечает его как посещенного и отправляет пользователя в канал unvisitedUsers. Эта функция также отслеживает, сколько пользователей обработали с помощью переменной count, делая это, мы можем проверить, достигли ли мы размера в строке 75, и закрыть канал usersCh, который завершит программу.

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

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

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

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

Я надеюсь, что вы нашли это письмо полезным и узнали что-то новое. Спасибо за чтение. Следите за обновлениями.