Конкурентность в Go
Конкурентность — это способность программы выполнять несколько задач одновременно.
Язык Go поддерживает конкурентность, предоставляя краткий и эффективный способ её реализации с помощью горутин и каналов.
Горутины:
- Единица конкурентного выполнения в Go, похожая на легковесные потоки.
- Планирование горутин управляется средой выполнения Go; пользователям не нужно вручную выделять потоки.
- Использование
goКлючевое слово запускает горутину. - Горутины неблокирующие и позволяют эффективно запускать десятки тысяч горутин.
Канал:
- Механизм, используемый для связи между горутинами в Go.
- Поддерживает синхронизацию и совместное использование данных, избегая явных механизмов блокировки.
- Использование
chanСоздаётся с помощью ключевого слова, через<-Оператор отправляет и получает данные.
Планировщик:
Планировщик Go основан на модели GMP. Планировщик назначает горутины системным потокам для выполнения и эффективно управляет конкурентностью за счёт координации M и P.
- G:Горутина。
- M: Системный поток (Machine).
- P: Логический процессор.
Горутина
Горутины — это легковесные потоки, а их планированием управляет среда выполнения Go.
Формат синтаксиса горутины:
go 函数名( 参数列表 )
Например:
go f(x, y, z)
Запуск новой горутины:
f(x, y, z)
Go позволяет оператору go запускать новый поток среды выполнения, а именно горутину, выполняя функцию в отдельной, только что созданной горутине. Все горутины в одной программе разделяют одно и то же адресное пространство.
Пример
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 5; i++ {
fmt.Println("Hello")
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go sayHello() // Запустить горутину
for i := 0; i < 5; i++ {
fmt.Println("Main")
time.Sleep(100 * time.Millisecond)
}
}
Выполнив приведённый выше код, вы увидите Main и Hello в выводе. Вывод не имеет фиксированного порядка, потому что они выполняются двумя горутинами:
Main Hello Main Hello ...
Канал
Каналы используются для передачи данных между горутинами.
Каналы можно использовать для синхронизированного выполнения и взаимодействия между двумя горутинами путем передачи значения указанного типа.
ИспользованиеmakeФункция создает канал, используя<-Оператор <- отправляет и принимает данные. Если направление не указано, это двунаправленный канал.
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
Объявление канала очень просто; мы просто используем ключевое слово chan. Канал должен быть создан до использования:
ch := make(chan int)
ПримечаниеПо умолчанию каналы небуферизованы. Когда отправитель отправляет данные, должен быть соответствующий получатель, одновременно принимающий данные.
В следующем примере используются две горутины для вычисления суммы чисел. После завершения вычислений горутинами вычисляется сумма двух результатов:
Пример
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // Отправить сумму в канал c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // Получить из канала c
fmt.Println(x, y, x+y)
}
Результат вывода:
-5 17 12
Буфер канала
Каналы могут иметь буфер, размер буфера задаётся вторым аргументом make:
ch := make(chan int, 100)
Буферизованные каналы позволяют отправке данных отправителем и приёму данных получателем находиться в асинхронном состоянии. То есть данные, отправленные отправителем, могут быть помещены в буфер и ожидать, пока получатель их извлечёт, вместо того чтобы требовать от получателя немедленного извлечения.
Однако, поскольку размер буфера ограничен, всё равно должен быть получатель для приёма данных. Иначе, как только буфер заполнится, отправитель не сможет отправить больше данных.
ПримечаниеЕсли канал небуферизован, отправитель блокируется до тех пор, пока получатель не получит значение из канала. Если канал буферизован, отправитель блокируется до тех пор, пока отправленное значение не будет скопировано в буфер; если буфер заполнен, это означает ожидание, пока какой-либо получатель извлечёт значение. Получатель блокируется до тех пор, пока не появится значение для приёма.
Пример
import "fmt"
func main() {
// Здесь мы определяем буферизованный канал, который может хранить целые числа
// Размер буфера равен 2
ch := make(chan int, 2)
// Поскольку ch — буферизованный канал, мы можем отправить два значения одновременно
// без необходимости немедленно синхронно читать данные
ch <- 1
ch <- 2
// Получить эти два значения
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Результат выполнения:
1 2
Перебор каналов и закрытие каналов в Go
Go использует ключевое слово range для перебора полученных данных, аналогично массивам или срезам. Формат следующий:
v, ok := <-ch
Если канал не получает данные, ok становится false, и затем канал можно закрыть с помощью close().close()функция для закрытия.
Пример
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// Функция range перебирает каждое значение, полученное из канала, потому что после того, как c отправил 10
// значения, он закрывает канал, поэтому здесь функция range, получив 10 значений
// затем завершается. Если канал c выше не закрыт, то функция range не
// завершится, и, таким образом, заблокируется при получении 11-го значения.
for i := range c {
fmt.Println(i)
}
}
Результат выполнения:
0 1 1 2 3 5 8 13 21 34
Оператор select
selectОператор select позволяет горутине ожидать несколько операций связи.selectОн будет блокироваться, пока один из его вариантов не сможет выполниться:
Пример
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
В приведённом выше коде,fibonacciгорутина на каналеcОтправьте последовательность Фибоначчи, и при полученииquitВыйти при получении сигнала из канала.
Результат выполнения:
0 1 1 2 3 5 8 13 21 34 quit
Использование WaitGroup
sync.WaitGroup используется для ожидания завершения нескольких горутин.
Синхронизация нескольких горутин:
Пример
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Вызовите Done(), когда горутина завершится
fmt.Printf("Воркер %d запущен\n", id)
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("Все воркеры завершили работу")
}
Для приведённого выше кода вывод при выполнении выглядит следующим образом:
Worker 1 started Worker 1 finished Worker 2 started Worker 2 finished Worker 3 started Worker 3 finished All workers done
Расширенные функции
Буферизованный канал:
Создать буферизованный канал.
ch := make(chan int, 2)
Контекст:
Используется для управления жизненным циклом горутин.
context.WithCancel、context.WithTimeout.
Мьютекс и RWMutex:
sync.Mutex предоставляет блокировку взаимного исключения для защиты общих ресурсов.
var mu sync.Mutex mu.Lock() // critical section mu.Unlock()
Итоги конкурентного программирования
Язык Go обеспечивает мощную поддержку конкурентности через горутины и каналы, упрощая сложность традиционной модели потоков. С помощью планировщика и инструментов синхронизации можно легко реализовать высокопроизводительные конкурентные программы.
- Горутины— это легковесный поток, использующий
goЗапускается с помощью ключевого слова. - КаналыИспользуются для связи между горутинами.
- Оператор selectИспользуется для ожидания нескольких операций с каналами.
ЧАВО
Взаимная блокировка:
- Пример: Все горутины ожидают, но данные недоступны.
- Решение: Избегайте бесконечного ожидания и правильно закрывайте каналы.
Гонка данных:
- Пример: Несколько горутин одновременно обращаются к одной и той же переменной.
- Решение: используйте Mutex или Channel для синхронизации доступа.