как вызвать cancel () при использовании exec.CommandContext в goroutine - PullRequest
0 голосов
/ 15 сентября 2018

Я хотел бы отменить по требованию текущую команду, для этого я пытаюсь, exec.CommandContext, в настоящее время пытаюсь это:

https://play.golang.org/p/0JTD9HKvyad

package main

import (
    "context"
    "log"
    "os/exec"
    "time"
)

func Run(quit chan struct{}) {
    ctx, cancel := context.WithCancel(context.Background())
    cmd := exec.CommandContext(ctx, "sleep", "300")
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    }

    go func() {
        log.Println("waiting cmd to exit")
        err := cmd.Wait()
        if err != nil {
            log.Println(err)
        }
    }()

    go func() {
        select {
        case <-quit:
            log.Println("calling ctx cancel")
            cancel()
        }
    }()
}

func main() {
    ch := make(chan struct{})
    Run(ch)
    select {
    case <-time.After(3 * time.Second):
        log.Println("closing via ctx")
        ch <- struct{}{}
    }
}

Проблема, с которой я сталкиваюсь, заключается в том, что вызывается cancel(), но процесс не прерывается, я предполагаю, что основной поток завершает работу первым и не ждет, пока cancel() завершит команду должным образом, главным образом потому, что если я использую time.Sleep(time.Second) в конце функции main, она завершает / убивает команду выполнения.

Есть идеи о том, как я мог wait убедиться, что команда была убита до выхода, не используя sleep? можно ли использовать cancel() в канале после успешного завершения команды?

В попытке использовать одну программу, я попробовал это: https://play.golang.org/p/r7IuEtSM-gL, но cmd.Wait(), кажется, все время блокирует select и не был доступен для вызова cancel()

1 Ответ

0 голосов
/ 15 сентября 2018

В 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()
}

( ссылка на игровую площадку )

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...