Goroutine
В языке Go сам язык реализовал и поддерживал параллелизм, нам нужно только передатьgo
ключевое слово для включенияgoroutine
Вот и все.
Gouroutine на самом деле является своего рода сопрограммой, похожей на сопрограмму в других языках, она многозадачна на уровне компилятора или виртуальной машины. Он может работать в одном или нескольких потоках, но, в отличие от потоков,неупреждающий, поэтому сопрограмма очень легкая.
func main() {
for i := 0; i < 1000; i++ {
go func(ii int) {
for {
fmt.Printf("Hello %d\n", ii)
}
}(i)
}
time.Sleep(time.Millisecond)
}
Приведенный выше код запускает 1000 сопрограмм и непрерывно выводит строки в течение 1 мс.Здесь следует отметить два момента:
-
time.Sleep
Перед выходом из основной функции Sleep занимает 1 мс. Это связано с тем, что при выходе из основной функции закрывается и ранее открытая сопрограмма.Если вы не заснете, вы не сможете увидеть информацию о печати. -
Анонимная функция передает переменную i как назначение параметра.
Если аргументы не переданы, можно использовать и переменную i, но по ссылке. И я постоянно увеличивается в основной функции, поэтому в информации о печати горутины невозможно узнать, какая сопрограмма печатается.
С точки зрения вывода информации ничем не отличается от открытия ветки, разве что номер другой. Но на уровне операционной системы потоки являются вытесняющими, а мы сказали, что сопрограммы не имеют вытеснения.Как это может быть одинаковым?
Причина вышеуказанной проблемы заключается в том, что при вызовеPrintf
Когда происходит переключение, горутина добровольно отказывается от управления. Мы модифицируем код следующим образом, чтобы продемонстрировать отсутствие вытеснения:
a := [10]int{}
for i := 0; i < 10; i++ {
go func(ii int) {
for {
a[ii]++
}
}(i)
}
time.Sleep(time.Millisecond)
fmt.Println(a)
Запустив приведенный выше код, возникает бесконечный цикл. Поскольку в первой открытой горутине она выполнялась в цикле.a[ii]++
, не уступил контроль; иmain
По сути, это также горутина, поэтому следующий код не выполняется и не завершается.
В этом случае мы можем активно отказаться от управления в горутине, например:
a[ii]++
runtime.Gosched()
Точки, в которых горутины могут переключаться (не гарантируется):
- I/O,select
- channel
- ожидание блокировки
- runtime.Gosched()
Модель параллелизма CSP
Go реализует две формы параллелизма:
- Общая память + синхронизация блокировки
- CSP, реализованный через горутины и каналы.
Модель параллелизма CSP — это концепция, предложенная примерно в 1970 г. Это относительно новая концепция.В отличие от традиционной многопоточной связи через общую память, CSP фокусируется на «совместном использовании памяти посредством связи».
Do not communicate by sharing memory; instead, share memory by communicating
Не общайтесь, делясь памятью, вместо этого делитесь памятью, общаясь.
channel
Канал используется для связи между различными горутинами, независимо от того, передается он или извлекается, он блокируется.
c := make(chan int)
c <- 1
Запуск приведенного выше кода напрямую приведет к взаимоблокировке:
all goroutines are asleep - deadlock!
Поэтому обычно открывайте горутину для получения канала перед использованием канала:
func createWorker() chan int {
c := make(chan int)
go func() {
for n := range c {
fmt.Println("received:", n)
}
}()
return c
}
func main() {
var channels [10]chan int
for i, _ := range channels {
channels[i] = createWorker()
}
for i, c := range channels {
c <- i
}
time.Sleep(time.Millisecond)
}
В приведенном выше коде мы определяемcreateWorker
, используемый для создания приемника и возврата канала. При этом мы можем ограничить возвращаемый канал, например:
func createWorker() chan<- int // 只能发送数据
func createWorker() <-chan int // 只能接收数据
обычно черезn := <- c
Для получения данных в приведенном выше примере используется диапазон, потому что канал может быть закрыт.
close(c)
Закройте канал, но рабочий процесс все еще может получить канал после его закрытия (до тех пор, пока горутина не завершится). Принятые данные представляют собой нулевое значение определенного канала, в приведенном выше примере принимается 0.
-
пройти через
n,ok := <- c
ок, чтобы определить, закрыт ли канал, также может быть получен через диапазон; -
Если вы записываете данные в канал, который был закрыт, он будет паниковать:
send on closed channel
. Не закрывайте канал от получателя и не закрывайте канал с несколькими одновременными отправителями
дождаться завершения задачи
В предыдущих примерах мы использовали метод Sleep для грубого управления выполнением задач, что определенно невозможно в реальном производстве. Я также сказал, что канал используется для связи, поэтому мы можем сообщить пользователю, что задача была выполнена через канал. Оптимизация кода выглядит следующим образом:
type worker struct {
in chan int
done chan bool
}
func createWorker() worker {
w := worker{
in: make(chan int),
done: make(chan bool),
}
go func(w worker) {
for n := range w.in {
fmt.Println("received:", n)
w.done <- true
}
}(w)
return w
}
func chanNormal() {
var workers [10]worker
for i, _ := range workers {
workers[i] = createWorker()
}
for i, w := range workers {
w.in <- i
}
for _, w := range workers {
<-w.done
}
}
func main() {
chanNormal()
}
В дополнение к определению нашего собственного канала, go также предоставляет намsync.WaitGroup
, чтобы управлять набором задач.
var wg sync.WaitGroup
wg.Add(1)
wg.Done()
wg.Wait()
Tip
Резюме сделано в структуре в методе, вcreate
реализуется, когдаworker
Вам не нужно заботиться о конкретном коде, просто вызовите метод done.