Синхронизация и безопасность в Go vs Java | Сoncurrency часть 2
Во второй части мы углубимся в синхронизацию и безопасность параллельного кода в Go. Для Java-разработчика полезно видеть аналоги: synchronized, ReentrantLock, CountDownLatch, атомарные операции и схемы работы с потоками. Рассмотрим основные инструменты Go: Mutex, RWMutex, WaitGroup, атомарные операции, race conditions, deadlocks и паттерны fan-in / fan-out.
1. Mutex / RWMutex
Mutex — механизм блокировки, который позволяет защищать доступ к общим ресурсам между goroutine. RWMutex позволяет разделять блокировку на чтение и запись: несколько читателей могут работать одновременно, но запись блокирует всех.
// Go: Mutex
var m sync.Mutex
counter := 0
m.Lock()
counter++
m.Unlock()
// RWMutex
var rw sync.RWMutex
rw.RLock() // чтение
value := counter
rw.RUnlock()
rw.Lock() // запись
counter = value + 1
rw.Unlock()
// Java: synchronized / ReentrantLock
ReentrantLock lock = new ReentrantLock();
int counter = 0;
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
// Для чтения/записи используется ReentrantReadWriteLock
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
try {
int value = counter;
} finally {
rw.readLock().unlock();
}
rw.writeLock().lock();
try {
counter = value + 1;
} finally {
rw.writeLock().unlock();
}
Используйте Mutex, когда нужен строгий контроль доступа. RWMutex эффективен для случаев, когда чтений больше, чем записей.
2. WaitGroup
WaitGroup позволяет ждать завершения группы goroutine. В Java аналогично CountDownLatch или Phaser.
// Go
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("Task", n)
}(i)
}
wg.Wait()
// Java
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
int n = i;
new Thread(() -> {
System.out.println("Task " + n);
latch.countDown();
}).start();
}
latch.await();
WaitGroup помогает безопасно ждать окончания нескольких задач. Важно не забывать делать Done() для каждой добавленной задачи.
3. Atomic operations
Для простой синхронизации счётчиков или флагов Go предоставляет пакет sync/atomic. В Java аналогично AtomicInteger и другие атомарные классы.
// Go
var counter int32 = 0
atomic.AddInt32(&counter, 1)
value := atomic.LoadInt32(&counter)
// Java
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
int value = counter.get();
Атомарные операции эффективны для простых счётчиков или флагов. Для сложных структур лучше использовать Mutex.
4. Race conditions
Race condition возникает, когда несколько goroutine одновременно изменяют общий ресурс без синхронизации. В Java аналогично многопоточные ошибки без synchronized/lock.
// Go: пример гонки
var counter int
for i := 0; i < 100; i++ {
go func() { counter++ }()
}
fmt.Println(counter) // может быть меньше 100 из-за гонки
Всегда анализируйте потенциальные race conditions. Go имеет инструмент go run -race для обнаружения гонок.
5. Deadlocks
Deadlock возникает, когда goroutine блокируют друг друга навечно, например, из-за циклических зависимостей на Mutex или каналах.
// Go: простой deadlock
ch := make(chan int)
ch <- 1 // никто не читает — блокировка
Deadlock трудно отлавливать. Планируйте порядок захвата ресурсов и используйте buffered каналы, где возможно.
6. Fan-in / Fan-out
Fan-out — распределение задач на несколько goroutine. Fan-in — сбор результатов в один канал. Это паттерн эффективного параллельного потока данных.
// Go: Fan-out / Fan-in
jobs := make(chan int, 5)
results := make(chan int, 5)
// Fan-out
for w := 1; w <= 3; w++ {
go func(id int) {
for j := range jobs {
results <- j*2
}
}(w)
}
// Отправляем задачи
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// Fan-in
for a := 1; a <= 5; a++ {
fmt.Println(<-results)
}
Fan-in / Fan-out позволяет масштабировать обработку задач и собирать результаты безопасно. Используйте buffered каналы для избегания блокировки.
Итог
В этой статье мы разобрали ключевые механизмы синхронизации в Go: Mutex / RWMutex для защиты ресурсов, WaitGroup для ожидания завершения задач, атомарные операции для простых счётчиков, а также рассмотрели race conditions, deadlocks и паттерны fan-in / fan-out.
Для Java-разработчика эти конструкции знакомы по функционалу: synchronized/ReentrantLock, CountDownLatch, AtomicInteger и другие. Важно понимать, когда использовать mutex, когда атомарные операции, и как безопасно собирать результаты из множества goroutine.
В следующих статьях мы рассмотрим более сложные паттерны многопоточности и оптимизации производительности, чтобы вы могли строить высоконагруженные и безопасные приложения на Go, используя опыт Java.
Галерея
Полезные статьи:
Новые статьи: