Golang中ContextWithTimeout的ctx.Done未被调用的原因分析
Golang中ContextWithTimeout的ctx.Done未被调用的原因分析 你好,
我是Go语言的新手,正在尝试理解为什么下面的代码无法正常工作。我创建了一个带超时的上下文,并在一个for循环中使用select语句,其中<-ctx.Done()是第一个case。我期望函数在超时发生时退出,但实际情况并非如此。有人能告诉我哪里做错了吗?
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("Ended at:", time.Now())
return
default:
fmt.Println(".")
}
}
}
更多关于Golang中ContextWithTimeout的ctx.Done未被调用的原因分析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
是的,那样也行。
更多关于Golang中ContextWithTimeout的ctx.Done未被调用的原因分析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
知道了,这很好。感谢指出这一点。
它在 Playground 上会一直运行下去。
从这里的回答来看,也可能是这样。看起来在紧密循环中并不推荐使用 ctx。
// 代码示例
只有当所有 case 都未就绪时,default 才会执行。 https://go.dev/tour/concurrency/6
说得好。我之前没意识到 select 是随机选择一个的。我原以为这些 case 是按优先级排列的。但是,第二个 case 并没有使用 select。
是的,我已经尝试过调试它。如果我添加打印语句,它们会显示即使超时时间已过,done 也从未被调用。但如果我在其中设置断点,它总是能正常工作。
这也无法退出
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
for ctx.Err() == nil {
fmt.Println(".", ctx, ctx.Err())
}
}
我认为第一个也不算正确。 由于默认分支总是就绪的,总是选择它也是合法的: select… 会在多个分支就绪时随机选择一个
但对我来说,两者都会终止。 你使用的是哪个平台/版本?
我使用的是 Go 1.22 版本。我尝试在 Go Playground 上分别用 1.21 和 1.22 版本运行,两者都失败了。我最初是在我的 Windows 机器上运行 Go 测试时发现这个问题的。不过,我在 Windows 机器上将其作为一个简单的应用程序运行时,运行正常。
这里的问题是 context 从未被取消。你使用了 defer。Defer 意味着函数将在定义它的函数退出时执行。由于你使用了无限循环,main 函数永远不会退出。要使用 context 退出循环,你需要在不使用 defer 的情况下调用 cancel()。
你尝试过调试它吗?你使用的是哪个系统?
如果是Windows机器,那么可能是因为系统时钟导致的时间定义问题,类似于Windows上的情况。这里有一个相关的问题:runtime: time.Sleep takes more time than expected on Windows (1ms -> 10ms) · Issue #44343 · golang/go · GitHub。其中一条评论建议尝试使用 windows.TimeBeginPeriod(1)。
windows.TimeBeginPeriod(1)
对于@Jorropo提出的在循环中加入某种等待/休眠操作以避免持续轮询的建议,这里提供另一种实现形式:
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("Ended at:", time.Now())
return
case <-time.Tick(1 * time.Second):
fmt.Println(".")
}
}
这也是我所想的。我原来的代码实际上是在默认情况下读取一些字节,并且会等待至少一个字节100毫秒。我使用bytes.Buffer为读取器编写了一个模拟,并在Read方法内部放置了100毫秒的等待来模拟该行为。在运行测试套件时,它会间歇性地失败,但单独运行时总是通过。也许100毫秒的等待时间不足以让Go处理ctx->Done。
func main() {
fmt.Println("hello world")
}
这些程序对我来说是有效的。 我不会认为它们是正确的,因为它们在轮询时会将CPU占用率推至100%。
由于你的核心数量和操作系统各不相同,轮询的效果可能会更好或更差。
类似这样的代码可以在playground上运行(而不仅仅是在我的CPU上):
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("Ended at:", time.Now())
return
default:
fmt.Println(".")
time.Sleep(time.Second / 4)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
for ctx.Err() == nil {
fmt.Println(".", ctx, ctx.Err())
time.Sleep(time.Second/4)
}
}
编写调度器的人试图让它能够处理此类情况,但似乎存在一个错误,导致它无法正确跟踪计时器或类似的东西。 即使调度器能够解决轮询循环的问题,这也是巨大的性能开销,因此你不应该故意编写这样的代码。
你的代码中,ctx.Done() 确实会在超时后被触发,但问题在于 default 分支。在 Go 的 select 语句中,如果多个 case 同时就绪,会随机选择一个执行。然而,default 分支是非阻塞的:只要没有其他 case 就绪,select 就会立即执行 default。在你的循环中,超时发生前,ctx.Done() 通道没有数据,因此每次循环都会进入 default 分支,打印 .。由于循环没有休眠或阻塞操作,它会极快地重复执行,可能在你看到超时输出前就已经打印了大量 .,甚至可能看起来像卡住。
实际上,超时确实会发生,ctx.Done() 也会被调用,但你需要让循环有机会检查到它。例如,在 default 分支中添加一个短暂的休眠,这样 CPU 就不会被完全占用,超时事件就有机会被处理:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
fmt.Println("Ended at:", time.Now())
return
default:
fmt.Println(".")
time.Sleep(100 * time.Millisecond) // 添加休眠
}
}
}
或者,更常见的做法是移除 default 分支,让 select 阻塞等待 ctx.Done(),但这样循环就不会执行其他操作。根据你的需求,如果需要在超时前执行任务,可以结合其他通道或条件。例如,使用 time.Ticker 定期执行任务:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
fmt.Println("Ended at:", time.Now())
return
case <-ticker.C:
fmt.Println(".")
}
}
}
这样,循环会每 100 毫秒打印一次 .,并在超时时退出。第一个代码示例中,添加 time.Sleep 后,你应该能看到 Ended at: 输出,证明 ctx.Done() 被正确调用。

