如何在超时后终止长时间运行的函数?

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

How to terminate long running function after a timeout

问题

我正在尝试在某个操作时间过长时关闭一个长时间运行的函数,也许这只是治标不治本的解决方案,但在我的情况下并没有真正起作用。

我是这样做的:

func foo(abort <-chan struct{}) {
    for {
        select {
        case <-abort:
            return
        default:
            // 长时间运行的代码
        }
    }
}

在另一个函数中,我在一段时间后关闭传递的通道,如果我删除了函数体,它会返回。然而,如果有一些长时间运行的代码,它不会影响结果,它会继续工作,好像什么都没有发生过。

感觉应该可以工作,但实际上并没有。我是否漏掉了什么?毕竟,路由器框架都有超时函数,超过一定时间后会终止正在运行的操作。也许这只是出于好奇,但我真的想知道如何做到这一点。

英文:

I a attempting to shut down a long running function if something takes too long, maybe is just a solution to treating the symptoms rather than cause, but in any case for my situation it didn't really worked out.

I did it like this:

func foo(abort &lt;- chan struct{}) {
for {
  select{
    case &lt;-abort:
      return
    default:
    ///long running code
  }
}
}

And in separate function I have which after some time closes the passed chain, which it does, if I cut the body returns the function. However if there is some long running code, it does not affect the outcome it simply continues the work as if nothing has happened.

It feels like it should work, but it does not. Is there anything I am missing. After all routers frameworks have timeout function, after which whatever is running is terminated. So maybe this is just out of curiosity, but I would really want how to do it.

答案1

得分: 2

你的代码在每次迭代之前只检查一次通道是否关闭,然后执行长时间运行的代码。在长时间运行的代码开始后,没有机会检查abort通道,因此它将一直运行到完成。

你需要在长时间运行的代码体中定期检查是否提前退出,这可以更符合惯用方式,例如使用context.ContextWithTimeout。你可以参考这个链接:https://pkg.go.dev/context#example-WithTimeout

英文:

your code only checks whether the channel was closed once per iteration, before executing the long running code. There's no opportunity to check the abort chan after the long running code starts, so it will run to completion.

You need to occasionally check whether to exit early in the body of the long running code, and this is more idiomatically accomplished using context.Context and WithTimeout for example: https://pkg.go.dev/context#example-WithTimeout

答案2

得分: 2

在你的“长时间运行的代码”中,你需要定期检查abort通道。

实现这种“定期”检查的常见方法是将代码分成多个块,每个块在合理的时间范围内完成(假设运行该进程的系统没有过载)。

在执行每个块之后,你检查终止条件是否成立,如果成立,则终止执行。
执行这种检查的惯用方法是使用“selectdefault”:

select {
case <-channel:
  // 终止处理
default:
}

在这里,如果channel没有准备好接收(或关闭),则立即执行default分支。

某些算法使得这种分块更容易,因为它们使用循环,每次迭代执行的时间大致相同。
如果你的算法不是这样的,你需要手动分块;在这种情况下,最好为每个块创建一个单独的函数(或方法)。

进一步的注意事项。

  1. 考虑使用contexts:它们提供了一个有用的框架来解决你正在解决的问题类型。

    更好的是,它们可以“继承”彼此,这样可以轻松实现两个好用的功能:

    • 你可以组合各种取消上下文的方式:例如,可以创建一个在某个超时时间过去或由其他代码显式取消时取消的上下文。
    • 它们使得创建“取消树”成为可能——当取消根上下文时,会将此信号传播到所有继承的上下文中,使它们取消其他goroutine正在执行的操作。
  2. 有时,当人们说“长时间运行的代码”时,他们并不是指代码实际上一直在CPU上进行计算,而是指代码执行对慢速实体(如数据库、HTTP服务器等)的请求,此时代码实际上并没有运行,而是在等待I/O以提供要处理的数据。

    如果你的情况是这样的,请注意,所有编写良好的Go包(当然,这包括所有处理网络服务的Go标准库包)在其API的那些实际上调用这些慢速实体的函数中接受上下文,这意味着如果你的函数接受一个上下文,你可以(实际上应该)将此上下文传递到适用的调用堆栈中,以便可以以与你的代码相同的方式取消调用的所有代码。


进一步阅读:

英文:

In your "long running code" you have to periodically check that abort channel.

The usual approach to implement that "periodically" is to split the code into chunks each of which completes in a reasonably short time frame (given that the system the process runs on is not overloaded).

After executing each such chunk you check whether the termination condition holds and then terminate execution if it is.
The idiomatic approach to perform such a check is "select with default":

select {
case &lt;-channel:
  // terminate processing
default:
}

Here, the default no-op branch is immediately taken if channel is not ready to be received from (or closed).

Some alogrithms make such chunking easier because they employ a loop where each iteration takes roughly the same time to execute.
If your algorithm is not like this, you'd have to chunk it manually; in this case, it's best to create a separate function (or a method) for each chunk.

Further points.

  1. Consider using contexts: they provide a useful framework to solve the style of problems like the one you're solving.

    What's better, the fact they can "inherit" one another allow one to easily implement two neat things:

    • You can combine various ways to cancel contexts: say, it's possible to create a context which is cancelled either when some timeout passes or explicitly by some other code.
    • They make it possible to create "cancellation trees" — when cancelling the root context propagates this signal to all the inheriting contexts — making them cancel what other goroutines are doing.
  2. Sometimes, when people say "long-running code" they do not mean code actually crunching numbers on a CPU all that time, but rather the code which performs requests to slow entities — such as databases, HTTP servers etc, — in which case the code is not actually running but sleeping on the I/O to deliver some data to be processed.

    If this is your case, note that all well-written Go packages (of course, this includes all the packages of the Go standard library which deal with networked services) accept contexts in those functions of their APIs which actually make calls to such slow entities, and this means that if you make your function to accept a context, you can (actually should) pass this context down the stack of calls where applicable — so that all the code you call can be cancelled in the same way as yours.


Further reading:

  • <https://go.dev/blog/pipelines>
  • <https://blog.golang.org/advanced-go-concurrency-patterns>

huangapple
  • 本文由 发表于 2022年9月6日 22:36:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/73623815.html
匿名

发表评论

匿名网友

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

确定