Горутины: параллельное программирование в 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: Параллельный поиск

Создайте программу, которая:

  1. Принимает список чисел
  2. Запускает несколько горутин для поиска заданного числа
  3. Возвращает результат, как только число найдено
  4. Использует каналы для синхронизации

Задание 2: Веб-скрапер

Напишите программу, которая:

  1. Принимает список URL
  2. Параллельно скачивает содержимое страниц
  3. Извлекает определённую информацию
  4. Сохраняет результаты в структурированном виде

Задание 3: Система обработки заказов

Создайте программу, которая:

  1. Принимает поток заказов
  2. Обрабатывает их параллельно
  3. Учитывает ограничения ресурсов
  4. Предоставляет статус обработки

Что дальше?

В следующем уроке мы:

  • Изучим каналы в Go
  • Познакомимся с паттернами параллельного программирования
  • Узнаем о конкурентных структурах данных
  • Начнём писать более сложные параллельные программы

🎯 Цель урока: К концу этого урока вы должны уметь:

  • Создавать и управлять горутинами
  • Синхронизировать параллельные операции
  • Обрабатывать ошибки в горутинах
  • Оптимизировать параллельные программы