【问题标题】:Golang: Why os.Exit doesn't work inside goroutinesGolang:为什么 os.Exit 在 goroutines 中不起作用
【发布时间】:2016-04-25 09:15:15
【问题描述】:

我有一个算法非常简单的研究程序。当成功即将到来时,goroutine 应该通过 os.Exit(0) 关闭(结束)。我等一天,两天……什么? :)

这是简单的代码

package main

import "os"

func main() {
    for {
        go func() { os.Exit(0) }()
    }
}

还有我的问题:

  1. 为什么 os.Exit 不终止 goroutine?
  2. 终止(停止)goroutine 执行的正确方法是什么?

游乐场:http://play.golang.org/p/GAeOI-1Ksc

【问题讨论】:

  • 你确定是这个原因吗?因为给定的代码对我来说很好。不管怎样,你试过简单的return吗?
  • 我不知道你在哪里试过,但在我的电脑上这段代码没有完成。我不知道如何将 return 与 goroutine 一起使用。 goroutine内部的所有计算,结果不能“返回”。
  • go func() { return }() ?
  • 而且我知道(手动计算后)成功结果被遗漏了......因为 os.Exit 在需要时没有工作。
  • 我添加了一个游乐场链接来显示我的代码逻辑(只是结构示例)

标签: go goroutine


【解决方案1】:

您遇到了 Go 调度程序的棘手问题。答案是os.Exit确实导致整个进程退出,但是按照你的方式,goroutines 永远不会运行。

可能发生的情况是 for 循环不断将新的 goroutine 添加到可用的 goroutine 列表中,但是由于整个进程只在一个 OS 线程中运行,Go 调度程序从未有时间实际调度不同的 goroutine,而只是继续运行那个 for 循环,而没有运行任何你产生的 goroutine。试试这个:

package main

import "os"

func main() {
    for {
        go func() { os.Exit(0) }()
        func() {}()
    }
}

如果你在 Go Playground 上运行它,它应该可以工作(事实上,here's a link)。

好的,上面的代码有效而您的无效,这一事实应该很神秘。这样做的原因是 Go 调度程序实际上是非抢占的。这意味着除非给定的 goroutine 自愿决定让调度程序选择运行其他东西,否则不会运行其他任何东西。

现在显然您从未编写过包含命令以使调度程序有机会运行的代码。发生的情况是,当您的代码被编译时,Go 编译器会自动将这些插入到您的代码中。以下是上述代码为何起作用的关键:goroutine 可能决定运行调度程序的时间之一是调用函数时。因此,通过添加 func() {}() 调用(显然什么都不做),我们允许编译器添加对调度程序的调用,让这段代码有机会调度不同的 goroutine。因此,其中一个衍生的 goroutine 运行,调用 os.Exit,然后进程退出。

编辑:如果编译器内联调用(或者,在这种情况下,因为它什么都不做,完全删除它),函数调用本身可能是不够的。另一方面,runtime.Gosched() 保证可以工作。

【讨论】:

  • 或者您可以将 GOMAXPROCS 更改为 >1 并且无需修改即可工作。当您在操场上发现 GOMAXPROCS 为 1 时,这一点非常明显。
  • 其实可能不是这样。如果他运行的是 go1.5,则 GOMAXPROCS 默认为他拥有的 CPU 内核数(几乎可以肯定超过 1)。问题是 GOMAXPROCS 不是要运行的操作系统线程数 - 它是要运行的操作系统线程的最大数,如果一开始只有一个运行,他的代码将永远不会启动调度程序产生新的。
  • (他说“我等一天,两天......什么?”,这让我觉得他是在电脑上运行它,而不仅仅是在操场上)。跨度>
  • 谢谢。我按照您的建议修改了我的示例,但没有成功play.golang.org/p/Io8NUXh0UR(过程耗时太长)
  • @AL,去很简单。当您使用 goroutine 和通道编写适当的惯用代码时,一切都会正常工作。但表面上简单的东西,里面却是复杂的。 Golang 的简单性是关于运行时的复杂性以及它为您做了多少。因此,当您发现像这样的边缘案例时,您必须深入挖掘以了解正在发生的事情。当您有一个没有函数调用的无限循环并且您的 GOMAXPROCS=1 时,死锁和 goroutines 饥饿是一种极端情况。这是一个已知的和预期的,但如果不了解 go scheduler 的基础知识就很难理解。
【解决方案2】:

你通过从函数返回来终止一个 goroutine。如果您需要确保 goruotine 运行完成,则需要等待 goroutine 完成。这通常通过sync.WaitGroup 或通过通道同步goroutines 来完成。

在您的示例中,您首先需要确保不可能产生无限数量的 goroutine。因为main 和新的 goroutine 之间没有同步点,所以不能保证它们中的任何一个都会在主循环运行时执行 os.Exit 调用。

等待任意数量的 goroutine 完成的通常方法是使用 sync.WaitGroup,这将确保它们在 main 退出之前全部执行。

wg := sync.WaitGroup{}
for i := 0; i < 10000; i++ {
    wg.Add(1)
    go func() { defer wg.Done() }()
}

wg.Wait()
fmt.Println("done")

【讨论】:

  • JimB,我在主题中添加了一个游乐场链接,您可以查看和修改我的示例以成功工作吗?谢谢。
【解决方案3】:

实现死手或终止开关

package main

import (
        "fmt"
        "time"
        "os"
)

const maxNoTickle = 50          // will bail out after this many no tickles
const maxWorking = 20           // pretendWork() will tickle this many times
const deadTicks = 250           // milliseconds for deadHand() to check for tickles
const reportTickles = 4         // consecutive tickles or no tickles to print something

var (
        tickleMe bool           // tell deadHand() we're still alive
        countNoTickle int       // consecutive no tickles
        countGotTickle int      // consecutive tickles
)

/**
*       deadHand() - callback to kill program if nobody checks in after some period
*/
func deadHand() {
        if !tickleMe {
                countNoTickle++
                countGotTickle = 0
                if countNoTickle > maxNoTickle {
                        fmt.Println("No tickle max time reached. Bailing out!")
                        // panic("No real panic. Just checking stack")
                        os.Exit(0)
                }
                if countNoTickle % reportTickles == 0 {
                        // print dot for consecutive no tickles
                        fmt.Printf(".")
                }
        } else {
                countNoTickle = 0
                countGotTickle++
                tickleMe = false        // FIXME: might have race condition here
                if countGotTickle % reportTickles == 0 {
                        // print tilda for consecutive tickles
                        fmt.Printf("~")
                }
        }
        // call ourselves again
        time.AfterFunc(deadTicks * time.Millisecond, deadHand)
}

/**
*       init() - required to start deadHand
*/
func init() {
        time.AfterFunc(250 * time.Millisecond, deadHand)
        tickleMe = true
}

/**
*       pretendWork() - your stuff that does its thing
*/
func pretendWork() {
        for count := 0; count < maxWorking; count++ {
                tickleMe = true // FIXME: might have race condition here
                // print W pretending to be busy
                fmt.Printf("W")
                time.Sleep(100 * time.Millisecond)
        }
}

func main() {
        go workTillDone()
        for {
                // oops, program went loop-d-loopy
        }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-07-15
    • 1970-01-01
    • 2016-02-19
    • 1970-01-01
    • 1970-01-01
    • 2018-11-05
    • 2020-04-19
    • 1970-01-01
    相关资源
    最近更新 更多