如何最好地保持一个长时间运行的Go程序持续运行?

huangapple go评论88阅读模式
英文:

How best do I keep a long running Go program, running?

问题

我有一个用Go语言编写的长时间运行的服务器。主函数启动了几个goroutine,程序的逻辑在这些goroutine中执行。之后,主函数没有做任何有用的事情。一旦主函数退出,程序就会终止。我目前使用的方法是简单地调用fmt.Scanln()来保持程序运行。我想知道其他人是如何防止主函数退出的。下面是一个基本的示例。在这里可以使用哪些想法或最佳实践?

我考虑过创建一个通道,并通过在该通道上接收来延迟主函数的退出,但我认为如果所有的goroutine在某个时刻变得不活跃,这可能会有问题。

附注:在我的服务器中(不是这个示例),程序实际上并没有连接到一个shell,所以与控制台交互并没有太多意义。目前这种方法可以工作,但我正在寻找“正确”的方法,假设存在这样的方法。

package main

import (
    "fmt"
    "time"
)

func main() {
    go forever()
    // 保持这个goroutine不退出
    // 以防止程序结束
    // 这是我问题的重点
    fmt.Scanln()
}

func forever() {
    for {
        // 一个可能无限运行的示例goroutine。
        // 在实际实现中,它可能会在通道接收上阻塞,而不是像time.Sleep这样。
        fmt.Printf("%v+\n", time.Now())
        time.Sleep(time.Second)
    }
}
英文:

I've a long running server written in Go. Main fires off several goroutines where the logic of the program executes. After that main does nothing useful. Once main exits, the program will quit. The method I am using right now to keep the program running is just a simple call to fmt.Scanln(). I'd like to know how others keep main from exiting. Below is a basic example. What ideas or best practices could be used here?

I considered creating a channel and delaying exit of main by receiving on said channel, but I think that could be problematic if all my goroutines become inactive at some point.

Side note: In my server (not the example), the program isn't actually running connected to a shell, so it doesn't really make sense to interact with the console anyway. For now it works, but I'm looking for the "correct" way, assuming there is one.

package main

import (
    "fmt"
    "time"
)

func main() {
    go forever()
    //Keep this goroutine from exiting
    //so that the program doesn't end.
    //This is the focus of my question.
    fmt.Scanln()
}

func forever() {
    for ; ; {
    //An example goroutine that might run
    //indefinitely. In actual implementation
    //it might block on a chanel receive instead
    //of time.Sleep for example.
        fmt.Printf("%v+\n", time.Now())
        time.Sleep(time.Second)
    }
}

答案1

得分: 64

永远阻塞。例如,

package main

import (
	"fmt"
	"time"
)

func main() {
	go forever()
	select {} // 永远阻塞
}

func forever() {
	for {
		fmt.Printf("%v+\n", time.Now())
		time.Sleep(time.Second)
	}
}
英文:

Block forever. For example,

package main

import (
	"fmt"
	"time"
)

func main() {
	go forever()
	select {} // block forever
}

func forever() {
	for {
		fmt.Printf("%v+\n", time.Now())
		time.Sleep(time.Second)
	}
}

答案2

得分: 33

Go的运行时设计假设程序员负责检测何时终止goroutine和何时终止程序。程序员需要计算goroutine和整个程序的终止条件。通过创建一个通道并延迟接收该通道上的数据来防止main函数退出是一种有效的方法。但是,这并不能解决检测何时终止程序的问题。

如果在main函数进入等待所有goroutine终止的循环之前无法计算goroutine的数量,那么需要发送增量,以便main函数可以跟踪有多少个goroutine正在运行:

// 接收goroutine数量的变化
var goroutineDelta = make(chan int)

func main() {
    go forever()

    numGoroutines := 0
    for diff := range goroutineDelta {
        numGoroutines += diff
        if numGoroutines == 0 { os.Exit(0) }
    }
}

// 概念代码
func forever() {
    for {
        if needToCreateANewGoroutine {
            // 在"go f()"之前而不是在f()内部执行此操作
            goroutineDelta <- +1

            go f()
        }
    }
}

func f() {
    // 当检测到此goroutine的终止条件时,执行以下操作:
    goroutineDelta <- -1
}

另一种方法是用sync.WaitGroup替换通道。这种方法的缺点是需要在调用wg.Wait()之前调用wg.Add(int),因此需要在main()中创建至少一个goroutine,而后续的goroutine可以在程序的任何部分创建:

var wg sync.WaitGroup

func main() {
    // 创建至少一个goroutine
    wg.Add(1)
    go f()

    go forever()
    wg.Wait()
}

// 概念代码
func forever() {
    for {
        if needToCreateANewGoroutine {
            wg.Add(1)
            go f()
        }
    }
}

func f() {
    // 当检测到此goroutine的终止条件时,执行以下操作:
    wg.Done()
}
英文:

The current design of Go's runtime assumes that the programmer is responsible for detecting when to terminate a goroutine and when to terminate the program. The programmer needs to compute the termination condition for goroutines and also for the entire program. A program can be terminated in a normal way by calling os.Exit or by returning from the main() function.

Creating a channel and delaying exit of main() by immediately receiving on said channel is a valid approach of preventing main from exiting. But it does not solve the problem of detecting when to terminate the program.

If the number of goroutines cannot be computed before the main() function enters the wait-for-all-goroutines-to-terminate loop, you need to be sending deltas so that main function can keep track of how many goroutines are in flight:

// Receives the change in the number of goroutines
var goroutineDelta = make(chan int)

func main() {
    go forever()

    numGoroutines := 0
    for diff := range goroutineDelta {
        numGoroutines += diff
        if numGoroutines == 0 { os.Exit(0) }
    }
}

// Conceptual code
func forever() {
    for {
        if needToCreateANewGoroutine {
            // Make sure to do this before &quot;go f()&quot;, not within f()
            goroutineDelta &lt;- +1

            go f()
        }
    }
}

func f() {
    // When the termination condition for this goroutine is detected, do:
    goroutineDelta &lt;- -1
}

An alternative approach is to replace the channel with sync.WaitGroup. A drawback of this approach is that wg.Add(int) needs to be called before calling wg.Wait(), so it is necessary to create at least 1 goroutine in main() while subsequent goroutines can be created in any part of the program:

var wg sync.WaitGroup

func main() {
    // Create at least 1 goroutine
    wg.Add(1)
    go f()

    go forever()
    wg.Wait()
}

// Conceptual code
func forever() {
    for {
        if needToCreateANewGoroutine {
            wg.Add(1)
            go f()
        }
    }
}

func f() {
    // When the termination condition for this goroutine is detected, do:
    wg.Done()
}

答案3

得分: 31

没有人提到signal.Notify(c chan<- os.Signal, sig ...os.Signal)

示例:

package main

import (
    "fmt"
    "time"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    go forever()

    quitChannel := make(chan os.Signal, 1)
    signal.Notify(quitChannel, syscall.SIGINT, syscall.SIGTERM)
    <-quitChannel
    //退出之前的清理时间
    fmt.Println("Adios!")
}

func forever() {
    for {
        fmt.Printf("%v+\n", time.Now())
        time.Sleep(time.Second)
    }
}
英文:

Nobody mentioned signal.Notify(c chan&lt;- os.Signal, sig ...os.Signal)

Example:

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
    &quot;os&quot;
    &quot;os/signal&quot;
    &quot;syscall&quot;
)

func main() {
    go forever()

    quitChannel := make(chan os.Signal, 1)
    signal.Notify(quitChannel, syscall.SIGINT, syscall.SIGTERM)
    &lt;-quitChannel
    //time for cleanup before exit
    fmt.Println(&quot;Adios!&quot;)
}

func forever() {
    for {
        fmt.Printf(&quot;%v+\n&quot;, time.Now())
        time.Sleep(time.Second)
    }
}

答案4

得分: 30

Go的runtime包中有一个名为runtime.Goexit的函数,它可以完全满足你的需求。

从主goroutine中调用Goexit会终止该goroutine,而不会返回到main函数。由于main函数尚未返回,程序会继续执行其他goroutine。如果所有其他goroutine都退出,程序将崩溃。

playground中的示例:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	go func() {
		time.Sleep(time.Second)
		fmt.Println("Go 1")
	}()
	go func() {
		time.Sleep(time.Second * 2)
		fmt.Println("Go 2")
	}()

	runtime.Goexit()

	fmt.Println("Exit")
}
英文:

Go's runtime package has a function called runtime.Goexit that will do exactly what you want.

> Calling Goexit from the main goroutine terminates that goroutine
without func main returning. Since func main has not returned,
the program continues execution of other goroutines.
If all other goroutines exit, the program crashes.

Example in the playground

package main

import (
    &quot;fmt&quot;
    &quot;runtime&quot;
    &quot;time&quot;
)

func main() {
    go func() {
	    time.Sleep(time.Second)
	    fmt.Println(&quot;Go 1&quot;)
    }()
    go func() {
	    time.Sleep(time.Second * 2)
	    fmt.Println(&quot;Go 2&quot;)
    }()

    runtime.Goexit()

    fmt.Println(&quot;Exit&quot;)
}

答案5

得分: 15

这是一个使用通道实现的简单的永久阻塞的代码块。

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan bool)
	go forever()
	<-done // 永久阻塞
}

func forever() {
	for {
		fmt.Printf("%v+\n", time.Now())
		time.Sleep(time.Second)
	}
}
英文:

Here is a simple block forever using channels

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
	done := make(chan bool)
	go forever()
	&lt;-done // Block forever
}

func forever() {
	for {
		fmt.Printf(&quot;%v+\n&quot;, time.Now())
		time.Sleep(time.Second)
	}
}

答案6

得分: 0

你可以使用Supervisor(http://supervisord.org/)将进程变为守护进程。你的函数forever只是一个它运行的进程,并且它将处理你的函数main的部分。你可以使用supervisor控制界面来启动/关闭/检查你的进程。

英文:

You could daemonize the process using Supervisor (http://supervisord.org/). Your function forever would just be a process that it runs, and it would handle the part of your function main. You would use the supervisor control interface to start/shutdown/check on your process.

huangapple
  • 本文由 发表于 2012年3月3日 13:45:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/9543835.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定