Параллельное программирование

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:

  1. Не читайте из закрытого канала.
  2. Не отправляйте данные в закрытый канал.
  3. Убедитесь, что все горутины завершаются корректно.

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("Главная функция завершена")
}

Использование контекста: #

  1. Управление временем выполнения.
  2. Передача данных между горутинами через ctx.Value().

Контексты упрощают управление жизненным циклом горутин и помогают избегать утечек памяти.