6. Параллельное программирование #
6.1 Горутины: основы и примеры #
Горутины — это лёгкие потоки выполнения, встроенные в язык Go. Они позволяют выполнять функции параллельно.
Создание горутины: #
go функция(аргументы)
Пример:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 5; i++ {
fmt.Println(i)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
go printNumbers() // Запускаем горутину
fmt.Println("Горутина запущена!")
time.Sleep(3 * time.Second) // Ждём, чтобы горутина успела завершиться
}
Важно: Горутины работают асинхронно. Если главная функция завершится раньше, горутины тоже завершатся.
6.2 Каналы: синхронизация и передача данных #
Каналы позволяют горутинам обмениваться данными безопасным способом.
Создание канала: #
ch := make(chan тип)
Отправка и получение данных: #
ch <- значение // Отправка
x := <-ch // Получение
Пример:
package main
import "fmt"
func sendMessage(ch chan string) {
ch <- "Привет из горутины!"
}
func main() {
ch := make(chan string)
go sendMessage(ch)
message := <-ch
fmt.Println(message) // Привет из горутины!
}
6.3 Буферизированные и небуферизированные каналы #
Небуферизированные каналы #
Передача данных блокирует отправителя, пока получатель не примет данные.
Пример:
ch := make(chan int) // Небуферизированный канал
Буферизированные каналы #
Могут хранить ограниченное количество сообщений без блокировки отправителя.
Пример:
ch := make(chan int, 2) // Канал с буфером на 2 значения
ch <- 1
ch <- 2
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
6.4 Закрытие каналов и предотвращение deadlock #
Каналы можно закрыть, чтобы сигнализировать о завершении отправки данных.
Закрытие канала: #
close(ch)
Проверка закрытия: #
value, ok := <-ch
if !ok {
fmt.Println("Канал закрыт")
}
Пример:
package main
import "fmt"
func sendNumbers(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go sendNumbers(ch)
for num := range ch { // Читаем, пока канал не закрыт
fmt.Println(num)
}
fmt.Println("Канал закрыт")
}
Предотвращение deadlock:
- Не читайте из закрытого канала.
- Не отправляйте данные в закрытый канал.
- Убедитесь, что все горутины завершаются корректно.
6.5 WaitGroup
и мьютексы
#
WaitGroup
#
Позволяет дождаться завершения нескольких горутин.
Пример:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Сообщаем о завершении
fmt.Printf("Горутина %d начала работу\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Увеличиваем счётчик
go worker(i, &wg)
}
wg.Wait() // Ожидаем завершения всех горутин
fmt.Println("Все горутины завершены")
}
Мьютексы #
Используются для защиты данных от конкурентного доступа.
Пример:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Итоговое значение:", counter)
}
6.6 Контексты (context.Context
)
#
Контексты позволяют управлять временем выполнения горутин и отменять их при необходимости.
Создание контекста: #
context.WithCancel
: Создаёт контекст, который можно отменить вручную.context.WithTimeout
: Автоматически отменяется через заданное время.
Пример:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Горутина завершена")
return
default:
fmt.Println("Работаю...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(3 * time.Second)
fmt.Println("Главная функция завершена")
}
Использование контекста: #
- Управление временем выполнения.
- Передача данных между горутинами через
ctx.Value()
.
Контексты упрощают управление жизненным циклом горутин и помогают избегать утечек памяти.