Golang中goroutine与context的使用问题探讨
Golang中goroutine与context的使用问题探讨 大家好,
我正在尝试学习 contexts。我写了下面这个简单的示例来测试 withCancel 的行为。我期望当我按下 Ctrl-C 中断时,在程序退出之前,Println("cancelled..")(第26行)会被打印到标准输出。然而,它并没有。:slight_smile: 有人能简单地解释一下我做错了什么,以及根据我最终期望的行为,我应该如何解决这个问题吗?
我相信关于使用上下文的概念,我(还)有些地方不清楚。我阅读了 effective-go 参考、Go 博客文章以及其他一些资料,但仍然看不出问题所在。:exploding_head:
提前感谢您的帮助。
以下是代码片段:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go infiniteForLoop(ctx)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
// 等待 SIGINT。
<-sig
}
func infiniteForLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("cancelled...")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("looping...")
}
}
}
更多关于Golang中goroutine与context的使用问题探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
感谢你的建议 @christophberger!
关于“done/ok channel”解决方案,我该如何为多个并发(可能并行)的 goroutine 实现这样的机制?类似于 WaitGroup.Add() 的概念。在这方面有什么好的实践吗?我想到的一个简单方法是为每个正在运行的 goroutine 创建一个新的“ok channel”(例如,一个 ok channel 的切片?),然后等待所有 channel 都被填充?但这看起来肯定不优雅/不容易实现…… 🤔 🤔
我相信应该有一种“标准”的方法来处理这些事情,因为一个典型的互联网服务器(例如聊天服务器)希望在停止/终止之前关闭连接……
再次提前感谢你宝贵的时间和支持!
更多关于Golang中goroutine与context的使用问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你好 @unique_gembom,
一个简单的技巧良好实践是仅将 main 函数用于启动应用程序。将所有更复杂的操作,包括那些需要延迟清理的操作,都放入一个单独的函数中。
func main() {
// 仅进行非常基础的设置
err := run()
if err != nil {
log.Println(err)
}
}
func run() error {
ctx, cancel := ...
defer cancel()
// 执行其他操作
return nil
}
在 run() 函数内部的所有延迟调用都可以在 main() 函数尝试退出之前完成。
使用一个“done”或“ok”通道来通知一个 goroutine 已完成工作是可以的。 或者,你可以使用 sync.WaitGroup 或 ErrGroup 来等待多个 goroutine。后者甚至允许 goroutine 返回错误。
我想我找到了问题所在。看起来 main() 函数在 sig 通道接收到信号后立即退出。这样就没有给 goroutine 执行其 println() 留出任何时间。
针对这一点,我可能需要实现第二个通道,用来阻塞 main() 函数,直到在 infiniteForLoop() 的 case ctx.Done() 中填充该通道。这还需要显式调用 cancel() 函数(而不是使用 defer)。所以代码大致如下(见下方):
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
//defer cancel()
ok := make(chan bool, 1)
go infiniteForLoop(ctx, ok)
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
// Wait for SIGINT.
<-sig
fmt.Println("interrupt received...waiting for loop to cancel...")
cancel()
<-ok
}
func infiniteForLoop(ctx context.Context, ok chan bool) {
for {
select {
case <-ctx.Done():
fmt.Println("cancelled...", ctx.Err())
ok <- true
return
default:
time.Sleep(2 * time.Second)
fmt.Println("looping...")
}
}
}
这是惯用的做法吗?还是有更好的方法建议?再次非常感谢!
在你的代码中,当收到 SIGINT 信号时,主函数会直接退出,而没有调用 cancel() 函数来取消上下文。defer cancel() 会在主函数返回时执行,但由于主函数在收到信号后直接退出,cancel() 没有被触发,导致 infiniteForLoop 中的 ctx.Done() 通道没有关闭,因此 "cancelled..." 不会被打印。
要解决这个问题,你需要在收到信号后显式调用 cancel(),并等待 goroutine 完成。以下是修改后的代码:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
infiniteForLoop(ctx)
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
<-sig
// 取消上下文,通知 goroutine 停止
cancel()
// 等待 goroutine 完成
wg.Wait()
}
func infiniteForLoop(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("cancelled...")
return
default:
time.Sleep(2 * time.Second)
fmt.Println("looping...")
}
}
}
在这个修改版本中,我们使用 sync.WaitGroup 来等待 infiniteForLoop 完成。当收到 SIGINT 信号时,主函数调用 cancel() 来取消上下文,然后通过 wg.Wait() 等待 goroutine 打印 "cancelled..." 并返回。这样就能确保在程序退出前打印出取消信息。

