Методы синхронизации между горутинами
В этом уроке мы изучим различные методы синхронизации между горутинами в Go. Правильная синхронизация — это ключ к созданию надёжных и эффективных параллельных программ.
Почему важна синхронизация?
Синхронизация горутин необходима для:
- Предотвращения гонок данных (race conditions)
- Координации работы между горутинами
- Безопасного доступа к общим ресурсам
- Эффективного использования системных ресурсов
💡 Интересный факт: В Go есть поговорка "Не общайтесь, разделяя память. Разделяйте память, общаясь". Это означает, что каналы предпочтительнее мьютексов для синхронизации.
Основные методы синхронизации
1. WaitGroup — ожидание завершения горутин
sync.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. Mutex — защита общих данных
sync.Mutex используется для защиты общих данных от одновременного доступа.
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Итоговый счётчик:", counter.Value())
}
3. RWMutex — оптимизированная блокировка
sync.RWMutex позволяет нескольким горутинам читать данные одновременно, но блокирует их при записи.
type SafeMap struct {
mu sync.RWMutex
data map[string]string
}
func (m *SafeMap) Get(key string) string {
m.mu.RLock()
defer m.mu.RUnlock()
return m.data[key]
}
func (m *SafeMap) Set(key, value string) {
m.mu.Lock()
defer m.mu.Unlock()
m.data[key] = value
}
Каналы для синхронизации
1. Небуферизированные каналы
func worker(done chan bool) {
fmt.Println("Работа начинается...")
time.Sleep(time.Second)
fmt.Println("Работа завершена")
done <- true
}
func main() {
done := make(chan bool)
go worker(done)
<-done // Ждём завершения
}
2. Буферизированные каналы
func main() {
ch := make(chan int, 3)
// Отправляем три значения без блокировки
ch <- 1
ch <- 2
ch <- 3
// Читаем значения
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
3. Select для работы с несколькими каналами
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second)
ch1 <- "сообщение 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "сообщение 2"
}()
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-time.After(3 * time.Second):
fmt.Println("таймаут")
}
}
Продвинутые техники
1. sync.Map для конкурентного доступа
func main() {
var m sync.Map
// Запись
m.Store("key", "value")
// Чтение
if value, ok := m.Load("key"); ok {
fmt.Println(value)
}
// Перебор
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true
})
}
2. sync.Cond для сложной синхронизации
type WorkerPool struct {
cond *sync.Cond
mu sync.Mutex
jobs int
}
func (p *WorkerPool) AddJob() {
p.mu.Lock()
p.jobs++
p.cond.Signal()
p.mu.Unlock()
}
func (p *WorkerPool) Worker() {
p.mu.Lock()
for p.jobs == 0 {
p.cond.Wait()
}
p.jobs--
p.mu.Unlock()
// Выполнение работы
}
Практические примеры
1. Пул воркеров с каналами
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
}
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: Параллельная обработка файлов
Создайте программу, которая:
- Читает несколько файлов параллельно
- Обрабатывает их содержимое
- Объединяет результаты
- Использует WaitGroup для синхронизации
Задание 2: Кэш с конкурентным доступом
Напишите потокобезопасный кэш, который:
- Хранит данные в map
- Использует RWMutex для оптимизации
- Поддерживает TTL для записей
- Обеспечивает атомарные операции
Задание 3: Система очередей
Создайте систему очередей, которая:
- Принимает задачи через каналы
- Обрабатывает их в пуле воркеров
- Ограничивает количество одновременных задач
- Предоставляет статус выполнения
Что дальше?
В следующем уроке мы:
- Изучим паттерны параллельного программирования
- Познакомимся с контекстами в Go
- Узнаем о конкурентных структурах данных
- Начнём писать более сложные параллельные программы
🎯 Цель урока: К концу этого урока вы должны уметь:
- Синхронизировать горутины различными способами
- Защищать общие данные от гонок
- Использовать каналы для коммуникации
- Оптимизировать параллельные программы