package main
import "fmt"
// Это простая функция, которую мы будем выполнять в горутине
func say(text string) {
for i := 0; i < 5; i++ {
fmt.Println(text)
}
}
func main() {
// Обычный вызов функции. main будет ждать, пока say("hello") завершится.
// Напечатает "hello" 5 раз, и только потом пойдет дальше.
say("hello")
} package main
import "fmt"
func say(text string) {
for i := 0; i < 5; i++ {
fmt.Println(text)
}
}
func main() {
// Запуск функции в новой горутине.
// main НЕ ЖДЕТ ее завершения. Он просто говорит: "Эй, Go Runtime,
// запусти, пожалуйста, say("world") в фоновом режиме", и сразу же
// продолжает выполнять свою работу.
go say("world")
// Поскольку main ничего больше не делает, он завершает свою работу.
// А когда завершается главная горутина (main), вся программа прекращается,
// даже если другие горутины еще не закончили работу.
fmt.Println("Программа завершена.")
} Программа завершена. package main
import (
"fmt"
"sync" // Импортируем пакет sync
"time"
)
func say(text string, wg *sync.WaitGroup) {
// В конце функции сообщаем, что работа горутины завершена.
// Используем defer, чтобы гарантировать, что Done() вызовется в любом случае,
// даже если внутри функции произойдет ошибка.
defer wg.Done()
for i := 0; i < 5; i++ {
fmt.Println(text)
time.Sleep(100 * time.Millisecond) // Добавим небольшую задержку
}
}
func main() {
// Создаем наш "список задач"
var wg sync.WaitGroup
// Добавляем в список 2 задачи, которые мы собираемся запустить
wg.Add(2)
// Запускаем первую горутину
go say("hello", &wg)
// Запускаем вторую горутину
go say("world", &wg)
// Ждем, пока обе горутины не сообщат о своем завершении.
// Программа "зависнет" на этой строке, пока счетчик wg не станет равен 0.
fmt.Println("Менеджер ждет, пока все задачи будут выполнены...")
wg.Wait()
// Эта строка выполнится только после того, как обе горутины завершатся
fmt.Println("Все задачи выполнены. Программа завершена.")
} Менеджер ждет, пока все задачи будут выполнены...
world
hello
world
world
hello
world
world
hello
hello
hello
Все задачи выполнены. Программа завершена. // racy_counter.go
package main
import (
"fmt"
"sync"
)
func main() {
var counter int
var wg sync.WaitGroup
// Запускаем 100 горутин, каждая увеличивает счетчик 100 раз
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
counter++ // <-- Гонка данных происходит здесь!
}
}()
}
wg.Wait()
fmt.Println("Итоговое значение счетчика:", counter)
fmt.Println("Ожидаемое значение: 10000")
} package main
import (
"fmt"
"sync"
"time"
)
// SafeCache - это потокобезопасный кэш
type SafeCache struct {
mu sync.RWMutex
items map[string]string
}
func NewSafeCache() *SafeCache {
return &SafeCache{
items: make(map[string]string),
}
}
// Get - операция чтения
func (c *SafeCache) Get(key string) (string, bool) {
// Блокируем на чтение. Другие горутины тоже могут вызывать Get() одновременно.
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.items[key]
return value, ok
}
// Set - операция записи
func (c *SafeCache) Set(key, value string) {
// Блокируем на запись. Ни одна другая горутина не может ни читать, ни писать.
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = value
}
func main() {
cache := NewSafeCache()
var wg sync.WaitGroup
// Запускаем 10 "читателей"
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
if val, ok := cache.Get("key"); ok {
fmt.Printf("Читатель #%d: %s\n", id, val)
} else {
fmt.Printf("Читатель #%d: ключ не найден\n", id)
}
}(i)
}
// Запускаем одного "писателя"
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Писатель: устанавливаю значение...")
time.Sleep(100 * time.Millisecond) // Чтобы читатели успели запуститься
cache.Set("key", "hello from writer")
fmt.Println("Писатель: значение установлено.")
}()
wg.Wait()
} Плюсы | Минусы |
Высокая производительность: Самый быстрый способ синхронизации для простых операций. | Сложность: Легко допустить ошибку. Код с atomic сложнее читать и отлаживать, чем с mutex. |
Не блокирует горутины: Не приводит к переключению контекста на уровень ОС. | Ограниченный набор типов: Работает только с базовыми типами (int32, int64, uintptr, pointer). |
Низкоуровневый контроль: Дает прямой доступ к инструкциям процессора. | Опасность: Неправильное использование может привести к очень сложным и редким багам. |
// НЕПРАВИЛЬНО
func raceCounter() {
var counter int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Println("Race Counter:", counter) // Непредсказуемый результат
} // ПРАВИЛЬНО, но медленнее
func mutexCounter() {
var counter int
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("Mutex Counter:", counter) // Всегда 1000
} // ПРАВИЛЬНО и БЫСТРО
func atomicCounter() {
// Важно: используем int64, так как atomic.AddInt64 требует int64
var counter int64
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Атомарно увеличиваем счетчик на 1
atomic.AddInt64(&counter, 1)
}()
}
wg.Wait()
// ВАЖНО: для чтения тоже нужно использовать atomic!
// Простое чтение fmt.Println(counter) может увидеть устаревшее значение из кэша CPU.
finalValue := atomic.LoadInt64(&counter)
fmt.Println("Atomic Counter:", finalValue) // Всегда 1000
} Ситуация | Рекомендуемый инструмент | Почему? |
Защита сложной структуры данных (мапа, слайс) | sync.Mutex | atomic не может работать со сложными типами. |
Простая операция, и производительность критична | sync/atomic | atomic будет значительно быстрее, так как нет накладных расходов на блокировку. |
Вы не уверены, что выбрать | sync.Mutex | Это безопаснее, проще и правильнее в 99% случаев. Производительность Mutex чаще всего достаточна. |