TL; TR: Пожалуйста, просто перейдите к последней части и скажите мне, как вы решите эту проблему.
Я начал использовать Голанг сегодня утром из Питона. Я хочу вызывать исполняемый файл с закрытым исходным кодом из Go несколько раз, с небольшим количеством concurrency, с разными аргументами командной строки. Мой итоговый код работает хорошо, но я хотел бы получить ваш вклад, чтобы улучшить его. Поскольку я нахожусь на раннем этапе обучения, я также объясню свой рабочий процесс.
Для простоты предположим, что эта "внешняя программа с закрытым исходным кодом" - это zenity, инструмент командной строки Linux, который может отображать графические окна сообщений из командной строки.
Вызов исполняемого файла из Go
Итак, в Go, я бы сделал следующее:
package main
import "os/exec"
func main() {
cmd := exec.Command("zenity", "--info", "--text='Hello World'")
cmd.Run()
}
Это должно работать правильно. Заметим, что .Run() является функциональным эквивалентом .Start(), за которым следует .Wait(). Это здорово, но если бы я хотел выполнить эту программу только один раз, весь материал программирования не стоил бы этого. Поэтому давайте просто делать это несколько раз.
Вызов исполняемого файла несколько раз
Теперь, когда я работал, я хотел бы несколько раз вызывать свою программу с помощью аргументов командной строки (здесь просто i для простоты).
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8 // Number of times the external program is called
for i:=0; i<NumEl; i++ {
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
}
Хорошо, мы сделали это! Но я до сих пор не вижу преимущества Go over Python... Этот кусок кода фактически выполняется в серийном режиме. У меня многоядерный процессор, и я бы хотел его использовать. Поэтому добавьте несколько concurrency с goroutines.
Goroutines, или способ сделать мою параллельную программу
a) Первая попытка: просто добавьте "go" s везде
Перепишите наш код, чтобы упростить вызов и повторное использование и добавить знаменитое ключевое слово go:
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8
for i:=0; i<NumEl; i++ {
go callProg(i) // <--- There!
}
}
func callProg(i int) {
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
Ничего! В чем проблема? Все горуты выполняются сразу. Я не знаю, почему zenity не выполняется, но AFAIK, программа Go вышла, прежде чем внешняя программа zenity могла даже быть инициализирована. Это было подтверждено использованием time.Sleep: ожидания на пару секунд было достаточно, чтобы 8 экземпляров zenity запустили себя. Я не знаю, можно ли это считать ошибкой.
Чтобы усугубить ситуацию, реальная программа, на которую я действительно звонил, требует времени, чтобы выполнить ее. Если я буду запускать 8 экземпляров этой программы параллельно на моем 4-ядерном процессоре, он потратит некоторое время на много переключение контекста... Я не знаю, как ведут себя обычные горуты Go, но exec.Command будет запустить zenity 8 раз в 8 различных потоков. Чтобы сделать это еще хуже, я хочу выполнить эту программу более 100 000 раз. Выполнение всего этого сразу в гортанах не будет эффективным вообще. Тем не менее, я бы хотел использовать свой 4-ядерный процессор!
b) Вторая попытка: использовать пулы goroutines
Интернет-ресурсы обычно рекомендуют использовать sync.WaitGroup для такого рода работ. Проблема с этим подходом заключается в том, что вы в основном работаете с партиями goroutines: если я создаю WaitGroup из 4 членов, программа Go будет ждать завершения всех 4 внешних программ, прежде чем вызывать новую партию из 4-х программ. Это неэффективно: процессор снова теряется впустую.
Некоторые другие ресурсы рекомендовали использовать буферный канал для выполнения работы:
package main
import (
"os/exec"
"strconv"
)
func main() {
NumEl := 8 // Number of times the external program is called
NumCore := 4 // Number of available cores
c := make(chan bool, NumCore - 1)
for i:=0; i<NumEl; i++ {
go callProg(i, c)
c <- true // At the NumCoreth iteration, c is blocking
}
}
func callProg(i int, c chan bool) {
defer func () {<- c}()
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
Это кажется уродливым. Каналы не предназначены для этой цели: я использую побочный эффект. Мне нравится концепция defer, но мне не нравится объявлять функцию (даже лямбду), чтобы вывести значение из созданного мной фиктивного канала. О, и, конечно, использование фиктивного канала само по себе является уродливым.
c) Третья попытка: умереть, когда все дети мертвы
Теперь мы почти закончили. Я должен только принять во внимание еще один побочный эффект: программа Go закрывается до того, как все всплывающие окна zenity закрыты. Это связано с тем, что когда цикл завершен (на восьмой итерации), ничто не мешает программе завершить. На этот раз полезно использовать sync.WaitGroup.
package main
import (
"os/exec"
"strconv"
"sync"
)
func main() {
NumEl := 8 // Number of times the external program is called
NumCore := 4 // Number of available cores
c := make(chan bool, NumCore - 1)
wg := new(sync.WaitGroup)
wg.Add(NumEl) // Set the number of goroutines to (0 + NumEl)
for i:=0; i<NumEl; i++ {
go callProg(i, c, wg)
c <- true // At the NumCoreth iteration, c is blocking
}
wg.Wait() // Wait for all the children to die
close(c)
}
func callProg(i int, c chan bool, wg *sync.WaitGroup) {
defer func () {
<- c
wg.Done() // Decrease the number of alive goroutines
}()
cmd := exec.Command("zenity", "--info", "--text='Hello from iteration n." + strconv.Itoa(i) + "'")
cmd.Run()
}
Готово.
Мои вопросы
- Знаете ли вы какой-либо другой правильный способ ограничить количество запущенных одновременно goroutines?
Я не имею в виду потоки; как Go управляет goroutines внутри, не имеет значения. Я действительно хочу ограничить количество запущенных goroutines сразу: exec.Command создает новый поток каждый раз, когда он вызывается, поэтому я должен контролировать количество времени, которое он вызывается.
- Отличается ли этот код к вам?
- Знаете ли вы, как избежать использования фиктивного канала в этом случае?
Я не могу убедить себя, что такие фиктивные каналы - это путь.