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
создает новый поток каждый раз, когда он вызывается, поэтому я должен контролировать количество времени, которое он вызывается.
- Отличается ли этот код к вам?
- Знаете ли вы, как избежать использования фиктивного канала в этом случае?
Я не могу убедить себя, что такие фиктивные каналы - это путь.