Введение в параллелизм в Go

Параллелизм — это сердце и душа современной разработки программного обеспечения, позволяющий программам выполнять несколько задач одновременно. Go с его легковесными потоками, называемыми горутинами, и встроенным механизмом связи, называемым каналами, делает параллелизм не только возможным, но и приятным. В этой статье мы углубимся в лучшие практики и шаблоны для создания параллельных приложений на Go.

Понимание горутин и каналов

Прежде чем мы перейдём к лучшим практикам, давайте кратко вспомним, что такое горутины и каналы.

Горутины

Горутины — это легковесные потоки, которые могут выполняться параллельно с основным потоком программы. Они планируются средой выполнения Go, которая эффективно управляет выполнением этих горутин.

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        time.Sleep(time.Second)
        fmt.Println("Hello from goroutine!")
    }()
    fmt.Println("Hello from main!")
    time.Sleep(2 * time.Second)
}

Каналы

Каналы — это основное средство связи между горутинами. Они позволяют безопасно и эффективно отправлять и получать данные.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)
    go func() {
        time.Sleep(time.Second)
        ch <- "Hello from goroutine!"
    }()
    msg := <-ch
    fmt.Println(msg)
}

Лучшие практики параллелизма в Go

1. Избегайте вложенности, сначала обрабатывая ошибки

Одной из ключевых лучших практик в Go является избегание глубокой вложенности в коде. Это особенно важно при работе с ошибками. Обрабатывая ошибки в первую очередь, вы сохраняете код чистым и удобочитаемым.

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("example.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    // Продолжайте с остальной частью кода
}

2. Избегайте повторения

Повторение в коде — признак плохого дизайна. Используйте функции и циклы, чтобы избежать повторяющегося кода.

package main

import (
    "fmt"
)

func printHello(name string) {
    fmt.Println("Hello, " + name)
}

func main() {
    names := []string{"Alice", "Bob", "Charlie"}
    for _, name := range names {
        printHello(name)
    }
}

3. Важный код идёт первым

Держите ваш важный код на верхнем уровне функций. Так его легче читать и понимать.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Важный код здесь
    fmt.Println("Запуск основной функции")

    // Менее важный код здесь
    go func() {
        time.Sleep(time.Second)
        fmt.Println("Привет от горутины!")
    }()
}

4. Документируйте свой код

Документация имеет решающее значение для любой кодовой базы. Используйте встроенные инструменты документации Go для документирования ваших функций и пакетов.

// Пакет main предоставляет пример параллельного приложения.
package main

import (
    "fmt"
    "time"
)

// main — точка входа в программу.
func main() {
    // Запускаем новую горутину
    go func() {
        time.Sleep(time.Second)
        fmt.Println("Hello from goroutine!")
    }()
    fmt.Println("Hello from main!")
    time.Sleep(2 * time.Second)
}

Шаблоны параллелизма в Go

1. Шаблон «веерное объединение»

Шаблон «веерное объединение» используется, когда вам нужно объединить несколько каналов в один. Это полезно, когда у вас есть несколько горутин, генерирующих данные, и вы хотите собрать их в одном месте.

package main

import (
    "fmt"
    "time"
)

func fanIn(inputs ...<-chan int) <-chan int {
    c := make(chan int)
    var wg sync.WaitGroup
    for _, input := range inputs {
        wg.Add(1)
        go func(input <-chan int) {
            defer wg.Done()
            for n := range input {
                c <- n
            }
        }(input)
    }
    go func() {
        wg.Wait()
        close(c)
    }()
    return c
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    go func() {
        for i := 5; i < 10; i++ {
            ch2 <- i
        }
        close(ch2)
    }()
    for n := range fanIn(ch1, ch2) {
        fmt.Println(n)
    }
}

2. Шаблон «веерообразное разветвление»

Шаблон «веерообразное разветвление» противоположен веерному объединению. Здесь вы распределяете данные из одного канала по нескольким каналам.

package main

import (
    "fmt"
    "time"
)

func fanOut(input <-chan int, num int) []<-chan int {
    var chs []<-chan int
    for i := 0; i < num; i++ {
        ch := make(chan int)
        chs = append(chs, ch)
        go func(ch chan int) {
            for n := range input {
                ch <- n
            }
            close(ch)
        }(ch)
    }
    return chs
}

func main() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
    }()
    chs := fanOut(ch, 3)
    for _, ch := range chs {
        go func(ch chan int) {
            for n := range ch {
                fmt.Println(n)
            }
        }(ch)
    }
    time.Sleep(2 * time.Second)
}

3. Шаблон пула рабочих процессов

Шаблон пула рабочих процессов полезен, когда у вас есть фиксированное количество рабочих, которым необходимо обрабатывать задачи параллельно.

package main

import (
    "fmt"
    "sync"
    "time"
)

func workerPool(tasks <-chan int, numWorkers int, wg *sync.WaitGroup) {
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for task := range tasks {
                fmt.Printf("Рабочий обрабатывает задачу %d\n", task)
                time.Sleep(time.Second)
            }
        }()
    }
}

func main() {
    tasks := make(chan int)
    var wg sync.WaitGroup
    go workerPool(tasks, 5, &wg)
    for i := 0; i < 10; i++ {
        tasks <- i
    }
    close(tasks)
    wg.Wait()
}

Использование контекста для отмены

Контексты в Go — мощный инструмент для управления жизненным циклом ваших горутин, особенно когда дело доходит до отмены.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    go func() {
        select {
        case <-ctx.Done():
            fmt.Println("Контекст отменён")
        case <-time.After(3 * time.Second):
            fmt.Println("Задача выполнена")
        }
    }()
    time.Sleep(3 * time.Second)
}

Методы синхронизации

Go предоставляет несколько методов синхронизации помимо каналов, включая sync.Mutex, sync.RWMutex и sync.WaitGroup.

Мьютекс

Мьютекс (сокращение от взаимного исключения) используется для защиты общих ресурсов от одновременного доступа.

package main

import (