В Go программа остановится, если будет достигнут конец метода main
(в пакете main
).Это поведение описано в спецификации языка Go в разделе о выполнении программы (выделение мое):
Выполнение программы начинается с инициализации пакета main
и последующего вызовафункция main
.Когда этот вызов функции возвращается, программа завершается. Он не ждет завершения других (не основных) процедур.
Дефекты
Я рассмотрю каждый из ваших примеров и связанные с нимиКонтроль дефектов потока.Ниже вы найдете ссылки на игровую площадку Go, но код в этих примерах не будет выполняться в изолированной программной среде ограничительной игровой площадки, поскольку не удается найти исполняемый файл sleep
.Скопируйте и вставьте в свою среду для тестирования.
Пример множественной процедуры
case <-time.After(3 * time.Second):
log.Println("closing via ctx")
ch <- struct{}{}
После того, как таймер сработает и вы дадите сигнал программе, пришло время убить ребенка и прекратить работу.нет ничего, что могло бы заставить метод main
блокировать и ждать завершения, поэтому он возвращается.В соответствии со спецификацией языка, программа завершается.
Планировщик может сработать после передачи по каналу, поэтому может быть гонка между main
выходом и другими программами, просыпающимися для получения от ch
,Однако небезопасно предполагать какое-либо конкретное чередование поведения - и для практических целей маловероятно, что какая-либо полезная работа произойдет до того, как main
выйдет.sleep
дочерний процесс будет осиротеть ;в системах Unix операционная система обычно переопределяет процесс на процесс init
.
Пример одиночной загрузки
Здесь возникает противоположная проблема: main
не возвращаетзначит, дочерний процесс не убит.Эта ситуация разрешается только при выходе из дочернего процесса (через 5 минут).Это происходит потому, что:
- Вызов
cmd.Wait
в методе Run
является блокирующим вызовом ( docs ).Оператор select
заблокирован, ожидая, пока cmd.Wait
вернет значение ошибки, поэтому не может получать данные из канала quit
. Канал quit
(объявлен как ch
in main
) небуферизованный канал .Операции отправки по небуферизованным каналам будут блокироваться до тех пор, пока получатель не будет готов принять данные.Из спецификации языка для каналов (опять же, выделите мое):
Емкость, в количестве элементов, устанавливает размер буфера в канале.Если емкость равна нулю или отсутствует, канал не буферизован, и обмен данными будет успешным только тогда, когда отправитель и получатель готовы .
Поскольку Run
заблокирован в cmd.Wait
нет готового получателя для получения значения, переданного по каналу оператором ch <- struct{}{}
в методе main
.main
блокирует ожидание передачи этих данных, что предотвращает возврат процесса.
Мы можем продемонстрировать обе проблемы с незначительными изменениями кода.
cmd.Wait
блокирует
Чтобы раскрыть характер блокировки cmd.Wait
, объявите следующую функцию и используйте ее вместо вызова Wait
.Эта функция является оболочкой с тем же поведением, что и cmd.Wait
, но с дополнительными побочными эффектами для печати того, что происходит с STDOUT.( Playground link ):
func waitOn(cmd *exec.Cmd) error {
fmt.Printf("Waiting on command %p\n", cmd)
err := cmd.Wait()
fmt.Printf("Returning from waitOn %p\n", cmd)
return err
}
// Change the select statement call to cmd.Wait to use the wrapper
case e <- waitOn(cmd):
После запуска этой модифицированной программы вы увидите вывод Waiting on command <pointer>
на консоль.После срабатывания таймеров вы увидите вывод calling ctx cancel
, но соответствующий текст Returning from waitOn <pointer>
отсутствует.Это произойдет только при возврате дочернего процесса, который вы можете быстро наблюдать, сократив продолжительность сна до меньшего количества секунд (я выбрал 5 секунд).
Отправка по каналу выхода, ch
, блоки
main
не может вернуться, потому что канал сигнала, используемый для распространения запроса на выход, не буферизован и соответствующего слушателя нет.Изменяя строку:
ch := make(chan struct{})
на
ch := make(chan struct{}, 1)
отправка по каналу в main
будет продолжаться (в буфер канала), а main
завершится - то же поведение, что и в примере с множественными программами.Однако эта реализация по-прежнему не работает: значение не будет считываться из буфера канала, чтобы фактически начать остановку дочернего процесса до возврата main
, поэтому дочерний процесс все еще будет потерян.
Исправленоверсия
Я подготовил исправленную версию для вас, код ниже.Есть также некоторые стилистические улучшения, чтобы преобразовать ваш пример в более идиоматический ход:
Переадресация по каналу, чтобы сигнализировать, когда пришло время остановиться, не нужна.Вместо этого мы можем избежать объявления канала, подняв объявление контекста и функцию отмены в методе main
.Контекст может быть отменен непосредственно в соответствующее время.
Я сохранил отдельную функцию Run
для демонстрации передачи контекста таким образом, но во многих случаях его логика может быть встроена в main
метод с вызовом процедуры для выполнения вызова блокировки cmd.Wait
.
- Оператор
select
в методе main
не нужен, так как имеет только один оператор case
. sync.WaitGroup
введен для явного решения проблемы выхода main
до того, как дочерний процесс (ожидаемый в отдельной программе) был убит.Группа ожидания реализует счетчик;вызов к Wait
блокам до тех пор, пока все goroutines не закончили работать и вызвал Done
.
package main
import (
"context"
"log"
"os/exec"
"sync"
"time"
)
func Run(ctx context.Context) {
cmd := exec.CommandContext(ctx, "sleep", "300")
err := cmd.Start()
if err != nil {
// Run could also return this error and push the program
// termination decision to the `main` method.
log.Fatal(err)
}
err = cmd.Wait()
if err != nil {
log.Println("waiting on cmd:", err)
}
}
func main() {
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(context.Background())
// Increment the WaitGroup synchronously in the main method, to avoid
// racing with the goroutine starting.
wg.Add(1)
go func() {
Run(ctx)
// Signal the goroutine has completed
wg.Done()
}()
<-time.After(3 * time.Second)
log.Println("closing via ctx")
cancel()
// Wait for the child goroutine to finish, which will only occur when
// the child process has stopped and the call to cmd.Wait has returned.
// This prevents main() exiting prematurely.
wg.Wait()
}
( ссылка на игровую площадку )