Горутины: параллельное программирование в Go
В этом уроке мы изучим одну из самых мощных возможностей Go — горутины. Это лёгкие потоки выполнения, которые позволяют создавать высокопроизводительные параллельные программы.
Почему горутины важны?
Горутины — это фундаментальная концепция в Go, которая:
- Позволяет эффективно использовать многоядерные процессоры
- Упрощает написание параллельного кода
- Обеспечивает высокую производительность
- Уменьшает накладные расходы на создание потоков
💡 Интересный факт: В Go можно запустить миллионы горутин на обычном компьютере, в то время как традиционные потоки обычно ограничены тысячами.
Основы горутин
1. Создание горутины
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("Работник %d начал работу\n", id)
time.Sleep(time.Second)
fmt.Printf("Работник %d закончил работу\n", id)
}
func main() {
// Запуск горутины
go worker(1)
// Даём время на выполнение
time.Sleep(2 * time.Second)
}
2. Анонимные горутины
func main() {
// Запуск анонимной горутины
go func(id int) {
fmt.Printf("Анонимный работник %d начал работу\n", id)
time.Sleep(time.Second)
fmt.Printf("Анонимный работник %d закончил работу\n", id)
}(1)
time.Sleep(2 * time.Second)
}
Синхронизация горутин
1. WaitGroup
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Уменьшаем счётчик при завершении
fmt.Printf("Работник %d начал\n", id)
time.Sleep(time.Second)
fmt.Printf("Работник %d закончил\n", id)
}
func main() {
var wg sync.WaitGroup
// Запускаем 5 работников
for i := 1; i <= 5; i++ {
wg.Add(1) // Увеличиваем счётчик
go worker(i, &wg)
}
wg.Wait() // Ждём завершения всех работников
fmt.Println("Все работники завершили работу")
}
2. Каналы для синхронизации
func worker(id int, done chan bool) {
fmt.Printf("Работник %d начал\n", id)
time.Sleep(time.Second)
fmt.Printf("Работник %d закончил\n", id)
done <- true // Сигнализируем о завершении
}
func main() {
done := make(chan bool, 5) // Буферизованный канал
// Запускаем работников
for i := 1; i <= 5; i++ {
go worker(i, done)
}
// Ждём завершения всех работников
for i := 1; i <= 5; i++ {
<-done
}
}
Продвинутые техники
1. Обработка ошибок в горутинах
func worker(id int, errChan chan error) {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("паника в работнике %d: %v", id, r)
}
}()
// Работа, которая может вызвать панику
if id == 3 {
panic("что-то пошло не так")
}
fmt.Printf("Работник %d выполнил работу\n", id)
errChan <- nil
}
2. Ограничение количества горутин
func processTasks(tasks []string, maxWorkers int) {
semaphore := make(chan struct{}, maxWorkers)
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
semaphore <- struct{}{} // Занимаем слот
go func(t string) {
defer func() {
<-semaphore // Освобождаем слот
wg.Done()
}()
// Обработка задачи
fmt.Printf("Обработка: %s\n", t)
time.Sleep(time.Second)
}(task)
}
wg.Wait()
}
Практические примеры
1. Параллельная обработка данных
func processData(data []int) []int {
result := make([]int, len(data))
var wg sync.WaitGroup
for i, v := range data {
wg.Add(1)
go func(idx int, value int) {
defer wg.Done()
// Тяжёлые вычисления
result[idx] = value * value
}(i, v)
}
wg.Wait()
return result
}
2. Пул воркеров
type WorkerPool struct {
tasks chan func()
wg sync.WaitGroup
}
func NewWorkerPool(size int) *WorkerPool {
pool := &WorkerPool{
tasks: make(chan func()),
}
pool.wg.Add(size)
for i := 0; i < size; i++ {
go func() {
defer pool.wg.Done()
for task := range pool.tasks {
task()
}
}()
}
return pool
}
Практические задания
Задание 1: Параллельный поиск
Создайте программу, которая:
- Принимает список чисел
- Запускает несколько горутин для поиска заданного числа
- Возвращает результат, как только число найдено
- Использует каналы для синхронизации
Задание 2: Веб-скрапер
Напишите программу, которая:
- Принимает список URL
- Параллельно скачивает содержимое страниц
- Извлекает определённую информацию
- Сохраняет результаты в структурированном виде
Задание 3: Система обработки заказов
Создайте программу, которая:
- Принимает поток заказов
- Обрабатывает их параллельно
- Учитывает ограничения ресурсов
- Предоставляет статус обработки
Что дальше?
В следующем уроке мы:
- Изучим каналы в Go
- Познакомимся с паттернами параллельного программирования
- Узнаем о конкурентных структурах данных
- Начнём писать более сложные параллельные программы
🎯 Цель урока: К концу этого урока вы должны уметь:
- Создавать и управлять горутинами
- Синхронизировать параллельные операции
- Обрабатывать ошибки в горутинах
- Оптимизировать параллельные программы