Понимание горутин и конкурентности в Go
January 04, 2021
В этой статье предполагается, что вы читали о Golang и знаете хотя бы основы.
Итак, позвольте мне начать с определения конкурентности (не путать с параллелизмом — в го именно конкурентность). Это просто выполнение некоторых инструкций кода одновременно. Вот и всё.
Горутины
Горутина — это лёгкий поток выполнения.
«Поток» или «поток выполнения» — это программный термин для базовой упорядоченной последовательности инструкций, которые могут быть переданы или обработаны одноядерным процессором. — stackoverflow.
Итак, горутина позволяет нам выполнять функции одновременно. Как это происходит? Простой пример:
package main
import (
"fmt"
"time"
)
func createUser(name string) {
fmt.Println(name + " успешно создан")
}
func uploadUserImage() {
fmt.Println("Изображение загружено")
}
func main() {
go createUser("Олег")
go uploadUserImage()
time.Sleep(time.Second)
}
Вывод:
Изображение загружено
Олег успешно создан
Таким образом, как вы можете видеть, результат этой простой программы не соответствует порядку вызовы функций, так как они выполняются одновременно. Поэтому результат может быть неупорядоченным. Но подождите минуту, почему я вызвал функцию sleep в конце? Это необходимо. Зачем это нужно?
Итак, программа Go завершит выполнение, когда завершится основная функция. А два наших вызова функций выполняются асинхронно в отдельных горутинах. Нужно подождать, пока они закончат, ведь главная функция никого ждать не будет, она даже не знает, что две функции всё ещё работают. Поэтому если мы удалим функцию sleep, то программа не будет ничего выводить на консоль. Попробуйте, если хотите.
Теперь перейдём к каналам.
Каналы
Каналы — это «трубы», соединяющие параллельно работающие горутины. Вы можете отправлять значения в каналы из одной горутины и получать эти значения в другой.
Давайте начнём непосредственно с кода, вот пример использования каналов.
package main
import (
"fmt"
)
func createUser(name string, done chan<- bool) {
fmt.Println(name + " успешно создан")
done <- true
}
func uploadUserImage(done chan<- bool) {
fmt.Println("Изображение загружено")
done <- true
}
func main() {
done := make(chan bool)
go uploadUserImage(done)
fmt.Println("Изображение загружено:", <-done)
go createUser("Олег", done)
fmt.Println("Пользователь создан:", <-done)
}
Итак, позвольте мне сначала рассказать вам, в чём разница между этим chan<-
, этим <-chan
и этим chan.
chan
=> в параметре функции означает, что мы будем читать и писать из этого канала, это тип данных;chan<-
=> означает, что мы будем только писать;<-chan
=> означает, что мы будем только читать.
Вывод этой программы:
Изображение загружено
Изображение загружено: true
Олег успешно создан
Пользователь создан: true
Поэтому в основной функции мы создали канал под названием done, мы передали этот канал в качестве параметра нашим рабочим (createUser
и upload
), и когда рабочий процесс завершён, мы уведомляем основную функцию. Мы выводим канал с именем channelName <- VALUE
И мы убираем сообщение с такого канала, как этот <-channelName
Если мы не уберём сообщение, функция горутина ничего не напечатает, потому что main
не будет ждать этого. Поэтому используя <-done
, мы говорим основной функции ждать, пока мы не получим сообщение от рабочего потока.
Поэтому, если мы удалим эту строку здесь:
fmt.Println("Пользователь создан:", <-done)
Мы получим такой вывод:
Изображение загружено
Изображение загружено: true
Так что <-done
просто блокировал выход из основной функции и, следовательно, прекращение выполнения программы.
И это то, что называется «синхронизацией каналов» (Channel Synchronization). Это просто модное слово, означающее блокировку до завершения выполнения горутины.
Но, для ожидания завершения нескольких горутин, вы можете предпочесть использовать WaitGroup.
WaitGroup
Чтобы дождаться завершения нескольких горутин, мы можем использовать WaitGroup
.
Опять же, давайте перейдём прямо к коду, а затем я объясню, как и почему.
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func createUser(name string) {
defer wg.Done()
fmt.Println(name + " успешно создан")
}
func uploadUserImage() {
defer wg.Done() // defer -> откладывает запуск функции до завершения тела метода
time.Sleep(time.Second * 3) // делаем вид, что это тяжёлая функция, и она долго что-то делает
fmt.Println("Изображение загружено")
}
func main() {
go createUser("Олег")
go uploadUserImage()
wg.Add(2)
wg.Wait()
}
Чтобы использовать WaitGroup, нам нужно импортировать пакет sync
, затем мы объявили глобальную переменную wg
.
Теперь позвольте мне объяснить, как работает WaitGroup. wg.Add(NUMBER_OF_GOROUTINES)
— эта функция увеличивает счётчик для запущенных горутин, wg.Done()
— будет уменьшать счётчик, поэтому каждый раз, когда функция завершает свою работу, вы вызываете функцию wg.Done(). Ключевое слово defer
просто говорит функции задержаться до тех пор, пока основная функция не завершит свою работу (под основной функцией я подразумеваю её родительскую функцию, которая является функцией загрузки и создания). Наконец, wg.Wait()
этой функции блокирует программу до тех пор, пока количество горутин не станет равным 0. Таким образом, wg.Done()
будет уменьшаться до тех пор, пока мы не достигнем 0. Затем wg.Wait()
запустится, и программа выйдет/завершится.