当前位置:首页 > 文章列表 > Golang > Go问答 > 竞争条件:读取子进程的标准输出和错误输出

竞争条件:读取子进程的标准输出和错误输出

来源:stackoverflow 2024-03-06 13:27:40 0浏览 收藏

目前golang学习网上已经有很多关于Golang的文章了,自己在初次阅读这些文章中,也见识到了很多学习思路;那么本文《竞争条件:读取子进程的标准输出和错误输出》,也希望能帮助到大家,如果阅读完后真的对你学习Golang有帮助,欢迎动动手指,评论留言并分享~

问题内容

在 go 中,我尝试:

  1. 启动子进程
  2. 分别从 stdout 和 stderr 读取
  3. 实施整体超时

经过多次谷歌搜索,我们找到了一些在大多数情况下似乎可以完成这项工作的代码。但似乎存在竞争条件,无法读取某些输出。

该问题似乎只发生在 linux 上,而不是 windows 上。

按照通过谷歌找到的最简单的解决方案,我们尝试创建一个带有超时的上下文:

context.withtimeout(context.background(), 10*time.second)

虽然这在大多数情况下都有效,但我们也能找到它永远挂起的情况。子进程的某些方面导致了死锁。 (与子进程没有充分分离的孙子进程有关,因此导致子进程永远不会完全退出。)

此外,似乎在某些情况下,当超时发生时返回的 error 将指示超时,但只会在进程实际退出后传递(从而使整个概念超时没用)。

func GetOutputsWithTimeout(command string, args []string, timeout int) (io.ReadCloser, io.ReadCloser, int, error) {
    start := time.Now()
    procLogger.Tracef("Initializing %s %+v", command, args)
    cmd := exec.Command(command, args...)

    // get pipes to standard output/error
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.StdoutPipe() error: %+v", err.Error())
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.StderrPipe() error: %+v", err.Error())
    }

    // setup buffers to capture standard output and standard error
    var buf bytes.Buffer
    var ebuf bytes.Buffer

    // create a channel to capture any errors from wait
    done := make(chan error)
    // create a semaphore to indicate when both pipes are closed
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        if _, err := buf.ReadFrom(stdout); err != nil {
            procLogger.Debugf("%s: Error Slurping stdout: %+v", command, err)
        }
        wg.Done()
    }()
    go func() {
        if _, err := ebuf.ReadFrom(stderr); err != nil {
            procLogger.Debugf("%s: Error  Slurping stderr: %+v", command, err)
        }
        wg.Done()
    }()

    // start process
    procLogger.Debugf("Starting %s", command)
    if err := cmd.Start(); err != nil {
        procLogger.Errorf("%s: failed to start: %+v", command, err)
        return emptyReader(), emptyReader(), -1, fmt.Errorf("cmd.Start() error: %+v", err.Error())
    }

    go func() {
        procLogger.Debugf("Waiting for %s (%d) to finish", command, cmd.Process.Pid)
        err := cmd.Wait()                                             // this can  be 'forced' by the killing of the process
        procLogger.Tracef("%s finished: errStatus=%+v", command, err) // err could be nil here
        //notify select of completion, and the status
        done <- err
    }()

    // Wait for timeout or completion.
    select {
    // Timed out
    case <-time.After(time.Duration(timeout) * time.Second):
        elapsed := time.Since(start)
        procLogger.Errorf("%s: timeout after %.1f\n", command, elapsed.Seconds())
        if err := TerminateTree(cmd); err != nil {
            return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), -1,
                fmt.Errorf("failed to kill %s, pid=%d: %+v",
                    command, cmd.Process.Pid, err)
        }
        wg.Wait() // this *should* take care of waiting for stdout and stderr to be collected after we killed the process
        return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), -1,
            fmt.Errorf("%s: timeout %d s reached, pid=%d process killed",
                command, timeout, cmd.Process.Pid)
    //Exited normally or with a non-zero exit code
    case err := <-done:
        wg.Wait() // this *should* take care of waiting for stdout and stderr to be collected after the process terminated naturally.
        elapsed := time.Since(start)
        procLogger.Tracef("%s: Done after %.1f\n", command, elapsed.Seconds())
        rc := -1
        // Note that we have to use go1.10 compatible mechanism.
        if err != nil {
            procLogger.Tracef("%s exited with error: %+v", command, err)
            exitErr, ok := err.(*exec.ExitError)
            if ok {
                ws := exitErr.Sys().(syscall.WaitStatus)
                rc = ws.ExitStatus()
            }
            procLogger.Debugf("%s exited with status %d", command, rc)
            return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), rc,
                fmt.Errorf("%s: process done with error: %+v",
                    command, err)
        } else {
            ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
            rc = ws.ExitStatus()
        }
        procLogger.Debugf("%s exited with status %d", command, rc)
        return ioutil.NopCloser(&buf), ioutil.NopCloser(&ebuf), rc, nil
    }
    //NOTREACHED: should not reach this line!
}

调用 getoutputswithtimeout("uname",[]string{"-mpi"},10) 将返回预期的单行输出大部分时间。但有时它会返回无输出,就好像读取 stdout 的 goroutine 没有足够快地启动来“捕获”所有输出(或提前退出?)“大多数时候”强烈建议竞争条件。

我们有时还会看到来自 goroutine 的有关“文件已关闭”的错误(这似乎是在超时条件下发生的,但也会在其他“正常”时间发生)。

我本以为在 cmd.start() 之前启动 goroutine 将确保不会丢失任何输出,并且使用 waitgroup 将保证它们在读取缓冲区之前完成。

那么我们是如何丢失输出的呢?两个“reader”协程和 cmd.start() 之间是否仍然存在竞争条件?我们是否应该确保这两个正在使用另一个 waitgroup 运行?

或者readfrom()的实现有问题吗?

请注意,由于与旧操作系统的向后兼容性问题,我们目前使用 go1.10,但 go1.12.4 也会出现相同的效果。

或者我们是否想得太多了,使用 context.withtimeout() 的简单实现就可以完成这项工作?


解决方案


这是不可能的,因为管道不能“丢失”数据。如果进程正在写入 stdout 而 go 程序尚未读取,则进程将阻塞。

解决该问题的最简单方法是:

  • 启动 goroutine 来收集 stdout、stderr
  • 启动一个计时器来终止进程
  • 启动流程
  • 使用 .wait() 等待它完成(或被计时器终止)
  • 如果计时器被触发,则返回超时错误
  • 处理等待错误
func GetOutputsWithTimeout(command string, args []string, timeout int) ([]byte, []byte, int, error) {
    cmd := exec.Command(command, args...)

    // get pipes to standard output/error
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return nil, nil, -1, fmt.Errorf("cmd.StdoutPipe() error: %+v", err.Error())
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        return nil, nil, -1, fmt.Errorf("cmd.StderrPipe() error: %+v", err.Error())
    }

    // setup buffers to capture standard output and standard error
    var stdoutBuf, stderrBuf []byte

    // create 3 goroutines: stdout, stderr, timer.
    // Use a waitgroup to wait.
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        var err error
        if stdoutBuf, err = ioutil.ReadAll(stdout); err != nil {
            log.Printf("%s: Error Slurping stdout: %+v", command, err)
        }
        wg.Done()
    }()
    go func() {
        var err error
        if stderrBuf, err = ioutil.ReadAll(stderr); err != nil {
            log.Printf("%s: Error Slurping stderr: %+v", command, err)
        }
        wg.Done()
    }()

    t := time.AfterFunc(time.Duration(timeout)*time.Second, func() {
        cmd.Process.Kill()
    })

    // start process
    if err := cmd.Start(); err != nil {
        t.Stop()
        return nil, nil, -1, fmt.Errorf("cmd.Start() error: %+v", err.Error())
    }

    err = cmd.Wait()
    timedOut := !t.Stop()
    wg.Wait()

    // check if the timer timed out.
    if timedOut {
        return stdoutBuf, stderrBuf, -1,
            fmt.Errorf("%s: timeout %d s reached, pid=%d process killed",
                command, timeout, cmd.Process.Pid)
    }

    if err != nil {
        rc := -1
        if exitErr, ok := err.(*exec.ExitError); ok {
            rc = exitErr.Sys().(syscall.WaitStatus).ExitStatus()
        }
        return stdoutBuf, stderrBuf, rc,
            fmt.Errorf("%s: process done with error: %+v",
                command, err)
    }

    // cmd.Wait docs say that if err == nil, exit code is 0
    return stdoutBuf, stderrBuf, 0, nil
}

理论要掌握,实操不能落!以上关于《竞争条件:读取子进程的标准输出和错误输出》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

版本声明
本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
在golang中返回动态数据的字符串格式在golang中返回动态数据的字符串格式
上一篇
在golang中返回动态数据的字符串格式
指针赋值:
下一篇
指针赋值:"*x=y"与"x=&y"是否等效?
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    508次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    497次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 互联网信息服务算法备案系统:如何完成算法备案流程
    互联网信息服务算法备案系统
    了解互联网信息服务算法备案系统,掌握如何进行算法备案的详细步骤和要求,确保您的互联网服务合规运营。
    57次使用
  • SEO标题魔匠AI:高质量学术写作平台,毕业论文生成与优化专家
    魔匠AI
    SEO摘要魔匠AI专注于高质量AI学术写作,已稳定运行6年。提供无限改稿、选题优化、大纲生成、多语言支持、真实参考文献、数据图表生成、查重降重等全流程服务,确保论文质量与隐私安全。适用于专科、本科、硕士学生及研究者,满足多语言学术需求。
    103次使用
  • PPTFake答辩PPT生成器:一键生成高效专业的答辩PPT
    PPTFake答辩PPT生成器
    PPTFake答辩PPT生成器,专为答辩准备设计,极致高效生成PPT与自述稿。智能解析内容,提供多样模板,数据可视化,贴心配套服务,灵活自主编辑,降低制作门槛,适用于各类答辩场景。
    135次使用
  • SEO标题Lovart AI:全球首个设计领域AI智能体,实现全链路设计自动化
    Lovart
    SEO摘要探索Lovart AI,这款专注于设计领域的AI智能体,通过多模态模型集成和智能任务拆解,实现全链路设计自动化。无论是品牌全案设计、广告与视频制作,还是文创内容创作,Lovart AI都能满足您的需求,提升设计效率,降低成本。
    257次使用
  • 美图AI抠图:行业领先的智能图像处理技术,3秒出图,精准无误
    美图AI抠图
    美图AI抠图,依托CVPR 2024竞赛亚军技术,提供顶尖的图像处理解决方案。适用于证件照、商品、毛发等多场景,支持批量处理,3秒出图,零PS基础也能轻松操作,满足个人与商业需求。
    124次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码