为了账号安全,请及时绑定邮箱和手机立即绑定

在 goroutine 中使用 exec.CommandContext 时如何调用 cancel()

在 goroutine 中使用 exec.CommandContext 时如何调用 cancel()

Go
偶然的你 2023-04-04 16:01:12
我想按需取消正在运行的命令,为此,我正在尝试,exec.CommandContext目前正在尝试:https://play.golang.org/p/0JTD9HKvyadpackage mainimport (    "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)在最后使用 amain它退出/终止正在运行的命令的功能。关于如何wait确保在不使用 a 退出之前命令已被终止的任何想法sleep?cancel()成功杀死命令后可以在频道中使用吗?在尝试使用单个 goroutine 时,我尝试了这个:https://play.golang.org/p/r7IuEtSM-gL但是cmd.Wait()似乎一直在阻塞select并且无法调用cancel()
查看完整描述

1 回答

?
慕容708150

TA贡献1831条经验 获得超4个赞

在 Go 中,如果到达main方法的末尾(在包中) ,程序将停止。main此行为在 Go 语言规范中有关程序执行的部分(强调我自己的)下进行了描述:

程序执行从初始化main包开始,然后调用函数main。当该函数调用返回时,程序退出。它不会等待其他(非主)goroutines 完成。


缺陷

我将考虑您的每个示例及其相关的控制流缺陷。您会在下面找到指向 Go playground 的链接,但这些示例中的代码不会在限制性 playground 沙箱中执行,因为sleep找不到可执行文件。复制粘贴到自己的环境进行测试。

多个 goroutine 示例

case <-time.After(3 * time.Second):
        log.Println("closing via ctx")
        ch <- struct{}{}

在计时器触发并且您向 goroutine 发出信号是时候杀死孩子并停止工作之后,没有什么可以导致方法main阻塞并等待它完成,所以它返回。根据语言规范,程序退出。

main调度程序可能会在通道传输后触发,因此在退出和其他 goroutine 醒来接收来自之间可能存在竞争ch。然而,假设任何特定的行为交错是不安全的——而且,出于实际目的,在main退出之前不太可能发生任何有用的工作。子sleep进程将成为孤儿;在 Unix 系统上,操作系统通常会将进程的父进程重放到进程上init

单个 goroutine 示例

在这里,你有相反的问题:main不返回,所以子进程没有被杀死。这种情况只有在子进程退出时(5 分钟后)才能解决。发生这种情况是因为:

  • cmd.Wait方法中的调用Run是阻塞调用 ( docs )。该select语句被阻塞等待cmd.Wait返回错误值,因此无法从quit通道接收。

  • 通道quit(声明为chmain是一个无缓冲通道。无缓冲通道上的发送操作将阻塞,直到接收方准备好接收数据。来自频道的语言规范(再次强调我自己的):

    以元素数量表示的容量设置通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有当发送方和接收方都准备好时,通信才会成功

    由于Run在 中被阻塞cmd.Wait,没有准备好的接收器来接收方法ch <- struct{}{}中的语句在通道上传输的值mainmain块等待传输此数据,从而阻止进程返回。

我们可以通过较小的代码调整来演示这两个问题。

cmd.Wait正在阻塞

要公开 的阻塞性质cmd.Wait,请声明以下函数并使用它代替调用Wait。此函数是一个包装器,具有与 相同的行为cmd.Wait,但有额外的副作用来打印 STDOUT 发生的情况。(游乐场链接):

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退出——与多 goroutine 示例相同的行为。然而,这个实现仍然是错误的:在返回之前,不会从通道的缓冲区中读取值来真正开始停止子进程main,所以子进程仍然是孤立的。


固定版

我已经为你制作了一个固定版本,代码如下。还有一些风格上的改进可以将您的示例转换为更惯用的 go:

  • 不需要通过通道间接发出停止时间的信号。相反,我们可以通过将上下文和取消函数的声明提升到方法来避免声明通道main。上下文可以在适当的时候直接取消。

    我保留了单独的Run函数来演示以这种方式传递上下文,但在许多情况下,它的逻辑可以嵌入到方法中main,并生成一个 goroutine 来执行cmd.Wait阻塞调用。

  • select方法中的语句是main不必要的,因为它只有一个case语句。

  • sync.WaitGroupmain引入是为了明确解决在子进程(在单独的 goroutine 中等待)被杀死之前退出的问题。等待组实现了一个计数器;对块的调用,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()

}


查看完整回答
反对 回复 2023-04-04
  • 1 回答
  • 0 关注
  • 166 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信