Предыстория #
Смотрел запись собеседования на должность middle Go разработчика в российский бигтех на youtube.com и увидел нетипичную задачу. Решил расписать один из вариантов её решения.
Для понимания того, что будет происходить далее, вы должны быть знакомы с основами языка программирования Go.
Самый простой способ пощупать Go – это пройти интерактивный Go tour от создателей языка.
Условие задачи #
Дан код:
package main
import (
"time"
"fmt"
)
func printNumber(num int) {
fmt.Printf("Process %d\n", num)
time.Sleep(time.Second)
}
func main() {
// ...
}
Необходимо:
- Последовательно напечатать числа от 1 до 10, используя функцию
printNumber
. Код функцииprintNumber
изменять нельзя. - Распараллелить печать чисел от 1 до 10. Порядок не важен. Печатать при помощи функции
printNumber
. Код функцииprintNumber
изменять нельзя. - Напечатать числа от 1 до 10 таким образом, чтобы за раз выводилось по 5 чисел. Порядок не важен. Печатать при помощи функции
printNumber
. Код функцииprintNumber
изменять нельзя.
Решение задачи 1 #
Проходимся в цикле от 1 до 10 и выводим числа, ничего сверхъестественного :)
package main
import (
"fmt"
"time"
)
func printNumber(num int) {
fmt.Printf("Process %d\n", num)
time.Sleep(time.Second)
}
func main() {
for i := 1; i <= 10; i++ {
printNumber(i)
}
}
Решение задачи 2 #
Идея решения: выполнить printNumber
для каждого числа в отдельной горутине. В основной горутине дождаться выполнения запущенных горутин.
Ожидать можно по разному (используя счетчики, подсчет значений из канала и прочие извращения), но самым простым вариантом будет использование структруы sync.Mutex
.
package main
import (
"fmt"
"sync"
"time"
)
func printNumber(num int) {
fmt.Printf("Process %d\n", num)
time.Sleep(time.Second)
}
func main() {
nums := make([]int, 0, 10)
for i := 1; i <= 10; i++ {
nums = append(nums, i)
}
printNumsParallel(nums)
}
func printNumsParallel(numbers []int) {
var wg sync.WaitGroup
wg.Add(len(numbers)) // 1
for _, num := range numbers {
// запуск горутин для печати
go func() {
defer wg.Done() // 2
printNumber(num) // 3
}()
}
wg.Wait() // 4
}
- Увеличиваем счетчик группы ожидания ДО запуска горутины.
- Через отложенный вызов вызываем уменьшение счетчик группы ожидания. Такой вызов отработает даже в том случае, если следующий за ним код в анонимной функции вызовет панику.
- Печатаем num. Начиная с Go 1.22 можно не передавать num в параметрах функции, т.к. на каждой итерации цикла переменная num будет создаваться заново, поэтому у каждой горутины num будет своя и уникальная (подробнее). Если вас попросят решить эту задачу в Go до 1.22, то num нужно будет явно передавать через параметры функции при запуске функции в отдельной горутине. Иначе поведение будет непрогнозируемое, но почти наверное вместо чисел от 1 до 10 вы получите что-то другое, т.к. порядок запуска горутин не определен.
- Ожидаем выполнение всех запущенных горутин.
Решение задачи 3 #
При решении текущей задачи хотелось бы каким-то образом “одновременно” запускать выполнение 5 печатающих задач. Для этих целей подходит паттерн конкурентного программирования worker pool.
Идея:
- Записать числа для печати (от 1 до 10) в буфферизированный канал с размером буффера, равным количеству выводимых чисел (5). После записи всех числе закрыть канал, чтобы читающие горутины могли остановить свою работу.
- Запустить горутины для печати чисел, которые будут вычитывать числа из канала. Количество горутин должно быть равно размеру буффера канала, это позволит создать эффект одновременной печати, который требуется в условиях задачи.
- Подождать пока все печатающие числа горутины отработают.
package main
import (
"fmt"
"sync"
"time"
)
func printNumber(num int) {
fmt.Printf("Process %d\n", num)
time.Sleep(time.Second)
}
func main() {
numbers := make([]int, 0, 10)
for i := 1; i <= 10; i++ {
numbers = append(numbers, i)
}
printNumbersOfKAtTime(numbers, 5)
}
func printNumbersOfKAtTime(numbers []int, k int) {
var wg sync.WaitGroup
wg.Add(k)
// создание буфферизированного канала для передачи
in := make(chan int, k)
for i := 0; i < k; i++ {
// запуск печатающих горутин
go func() {
defer wg.Done()
// каждая горутина вычитывает из канала до тез пор, пока он не будет закрыт
for num := range in {
printNumber(num)
}
}()
}
// запуск горутины для записи числе в канал, из которого будут читать печатающие горутины.
// Её можно не отслеживать в счетчике wg,
// т.к. читающие горутины не завершат свою работу до тех пор, пока не отработает эта.
go func() {
for _, num := range numbers {
in <- num
}
// закрываем канал после записи
close(in)
}()
// ожидаем выполнение всех печатающих горутин
wg.Wait()
}