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

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

Чем отличается конкурентность на Go?

Ее отличие существенно. А основной принцип выражается фразой: «Совместно использовать память, взаимодействуя, а не взаимодействовать, разделяя память». При таком подходе упрощается контроль взаимодействия приложений, предлагаются более безопасные и простые методы управления общей памятью.

Модель взаимодействующих последовательных процессов

Модель конкурентности Go основана на теории Тони Хоара о взаимодействующих последовательных процессах, в рамках которой определяются взаимодействие и синхронизация параллельных процессов. На Go эта теория применяется практически. Горутины и каналы  —  ключевые компоненты модели, они эффективно используются на Go.

Основные понятия

1. Горутины

Это базовая единица конкурентности на Go. Они намного легче традиционных потоков и управляются средой выполнения Go. Горутинами осуществляется конкурентное выполнение, они быстро запускаются.

Особенности:

  • Низкий расход памяти  —  около 2 Кб.
  • Быстрый запуск.
  • Автоматическое масштабирование.
  • Интеллектуальное планирование средой выполнения Go.

Сценарии:

  • Одновременная обработка HTTP-запросов на веб-серверах.
  • Параллельные операции в микросервисах.
  • Системы обработки больших данных.
  • Анализ данных в реальном времени.

2. Каналы

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

package main

import (
"fmt"
"sync"
)

func generateNumbers(ch chan int, wg *sync.WaitGroup) {
defer wg.Done() // «WaitGroup» уведомляется, когда горутина завершена
for i := 1; i <= 5; i++ {
ch <- i // Данные отправляются на канал
}
}

func printNumbers(ch chan int, wg *sync.WaitGroup) {
defer wg.Done() // «WaitGroup» уведомляется, когда горутина завершена
for i := 1; i <= 5; i++ {
num := <-ch // Из канала получаются данные
fmt.Println("Received:", num)
}
}

func main() {
ch := make(chan int) // Создается канал
wg := sync.WaitGroup{} // Инициализируется «WaitGroup»

wg.Add(2) // Дождемся двух горутин

go generateNumbers(ch, &wg) // Запускается горутина для генерирования чисел
go printNumbers(ch, &wg) // Запускается горутина для вывода чисел

wg.Wait() // Ожидается завершение обеих горутин
fmt.Println("All numbers received and printed.")
}

В этом коде показано взаимодействие горутин через каналы Go. Горутиной generateNumbers на канал ch отправляются числа от 1 до 5, которые принимаются и выводятся горутиной printNumbers. Каналами синхронизируется поток данных между горутинами, обеспечивается корректная последовательность.

Обратимся к WaitGroup и ее роли в синхронизации горутин.

Продвинутый уровень: WaitGroup и мьютекс для синхронизации

Конкурентности на Go, особенно при управлении общими ресурсами, требуется пристальное внимание. Мьютекс и WaitGroup  —  это мощные инструменты для решения проблем синхронизации.

1. WaitGroup

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

Пример:

package main

import (
"fmt"
"sync"
)

func task(id int, wg *sync.WaitGroup) {
defer wg.Done() // Вызывается, когда горутина завершена
fmt.Printf("Task %d started\n", id)
}

func main() {
wg := sync.WaitGroup{} // Для отслеживания ожидаемых горутин создается «WaitGroup»
for i := 1; i <= 5; i++ {
wg.Add(1) // Количество ожидаемых горутин увеличивается
go task(i, &wg) // Запускается каждая горутина
}

wg.Wait() // Ожидается завершение всех горутин
fmt.Println("All tasks completed") // По завершении всех горутин выводится сообщение
}

В этом коде показано ожидание завершения горутин с использованием sync.WaitGroup. Функция task выполняется в пяти горутинах, каждой из которых выводится сообщение. Счетчик для каждой горутины увеличивается в wg.Add(1), а о завершении горутины сигнализируется в wg.Done(). Наконец, в wg.Wait() ожидается завершение всех горутин, после чего выводится All tasks completed.

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

2. Мьютекс

Мьютексом гарантируется, что доступ к общему ресурсу одномоментно получается только одной горутиной. Так предотвращаются состояния гонок. Мьютекс «блокируется» горутиной до получения доступа к общему ресурсу, а после завершения операции  —  «разблокируется».

Пример:

package main

import (
"fmt"
"sync"
)

var counter int
var lock sync.Mutex

func increment() {
lock.Lock() // Блокировка захватывается
counter++ // Общий ресурс обновляется
lock.Unlock() // Блокировка освобождается
}

func main() {

wg := sync.WaitGroup{} // Для ожидания завершения горутин создается «WaitGroup»

for i := 0; i < 1000; i++ {
wg.Add(1) // Количество ожидаемых горутин увеличивается
go func() {
defer wg.Done() // Когда горутина завершается, счетчик уменьшается
increment() // Счетчик увеличивается
}()
}

wg.Wait() // Ожидается, пока не завершатся все горутины
fmt.Println("Counter: ", counter) // Выводится конечное значение счетчика
}

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

Реальные применения

Функционал конкурентного программирования Go широко применяется в современных программных приложениях. Вот примеры.

1. Архитектуры микросервисов

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

  • Межсервисное взаимодействие.
  • Балансировка нагрузки.
  • Остановка запросов.
  • Обнаружение сервисов.

2. Обработка данных в реальном времени

Go  —  отличный выбор для приложений обработки данных в реальном времени. Финансовыми системами, устройствами интернета вещей и аналитическими платформами функционал конкурентного программирования Go применяется для:

  • Потоковой обработки.
  • Порождения событий.
  • Метрик реального времени.
  • Агрегирования данных.

3. Веб-приложения

В современных веб-приложениях функционалом конкурентного программирования Go оптимизируется производительность:

  • Конкурентная обработка запросов.
  • Подключения к веб-сокетам.
  • Фоновая обработка заданий.
  • Управление кэшем.

Рекомендации и типичные ошибки

При написании программ с конкурентным выполнением разработчиками учитываются важные аспекты:

  • Утечки горутин: обеспечиваются корректное завершение горутин и эффективное использование контекстов.
  • Управление каналами: корректно закрываются каналы и обрабатываются ошибки с nil.
  • Обработка ошибок: корректно обрабатываются ошибки и применяются механизмы восстановления при панике.
  • Управление ресурсами: оптимизируется использование памяти и процессора.
  • Взаимоблокировка: применением порядка захвата блокировок или тайм-аутами предотвращается неопределенное ожидание горутины.

Заключение

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

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ali Rıza Aynacı: Concurrency and Synchronization in Go: Using Goroutines, Mutex, and WaitGroup

Предыдущая статья11 малоизвестных, но полезных приемов по фронтенду
Следующая статья5 функций-расширений в арсенале каждого разработчика Jetpack Compose