Введение в параллелизм в 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 (