Golang Go语言中如何关闭正在运行的协程?谢谢

发布于 1周前 作者 yuanlaile 来自 Go语言
package main

import ( “fmt” “sync” “time” )

func workRoutine(work chan string, done chan string, wg *sync.WaitGroup) { select { case donemsg := <-done: fmt.Println(donemsg) case msg := <-work: //这里将有大量工作可能半个小时都未必执行完,这样 done 这个 channel 就无法收到退出信号 for i := 0; i < 100000; i++ { fmt.Println(msg) time.Sleep(time.Second) } case <-time.After(time.Second * 1000): fmt.Println(“timeout”) } wg.Done() } func closeWorkRoutine(done chan string, wg *sync.WaitGroup) { //目标是 10 秒后希望能关掉 test 这个协程 time.Sleep(time.Second * 10) done <- “done” wg.Done() } func main() { done := make(chan string) work := make(chan string, 1) wg := sync.WaitGroup{} wg.Add(2) work <- “work” go workRoutine(work, done, &wg) go closeWorkRoutine(done, &wg) wg.Wait() }

请参考上面的代码,我现在有两个协程,一个叫 workRoutine ,另一个叫 closeWorkRoutine ,我的目标是希望 closeWorkRoutine 可以在 10 秒后可以关闭 workRoutine 的协程,但是上面这个代码是无法关闭的,因为 work 过于繁重,永远轮不到执行关闭的时候。请问有什么办法可以直接关闭协程,而无需介意当前协程的状态,最好能像线程那样有一个 ID 我可以直接在外部强制关掉,请问我应该如何做呢,谢谢。


Golang Go语言中如何关闭正在运行的协程?谢谢

更多关于Golang Go语言中如何关闭正在运行的协程?谢谢的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

38 回复

context

更多关于Golang Go语言中如何关闭正在运行的协程?谢谢的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


楼上正解

如果想中途 cancel 掉,可以使用 context.WithCancel

严格来说是没办法的,只有发生上下文切换的时候你才有机会执行退出逻辑,如果协程阻塞在某个操作你没有办法去关闭
阻塞了 context 什么的都没用

关闭掉的,context 只是 channel 发一个通知

添加事件机制? work 里面的循环,每干完一轮活,就检查是否有退出事件?

package main

import (
“context”
“time”
)

var work = make(chan struct{})

func workRoutine(ctx context.Context) {

f1 := func(ctx context.Context) {
select {
case <-ctx.Done():
default:

}
}
for {
select {
case <-ctx.Done():
break
case <-work:
// 任务分解 , 分别限时
c1, _ := context.WithTimeout(ctx, 10time.Second)
f1(c1)
}
}
}
func main() {
c, cancel := context.WithTimeout(context.Background(), 10
time.Minute)
defer cancel()
go workRoutine©
select {}
}

不知道这样能不能满足你的要求

执行每一个子任务的时候加上时间限制,并且没执行完一次子任务就检查一下是否需要退出。

func workRoutine(work chan string, done chan string, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case donemsg := <-done:
fmt.Println(donemsg)
return
case <-time.After(time.Second * 1000):
fmt.Println(“timeout”)
return
case msg := <-work:
fmt.Println(msg)
time.Sleep(time.Second)
fmt.Println(“work done”)
}
}
}

这样的?

`
for (i:=0;i<100000;i++){
select {
case donemsg := <-done:
//stop
default:
//do something
}
}

context 要看怎么用,我的建议是将 context 继续向下传递。

"//这里将有大量工作可能半个小时都未必执行完,这样 done 这个 channel 就无法收到退出信号"
这里如果是一个执行很长的代码,从外部你无法干掉的。将 context 继续传入这个函数,以及它的子函数,即 context 来控制整个调用链路上的超时。

根据目前信息,就这个思路最简单了。

关闭不了的

go<br><br>package main<br><br>import (<br> "log"<br> "sync"<br> "time"<br>)<br><br>var wg sync.WaitGroup<br><br>func worker(quit &lt;-chan int) {<br> defer wg.Done()<br> for {<br> select {<br> case &lt;-quit:<br> log.Printf("收到退出信号")<br> return // 必须 return ,否则 goroutine 是不会结束的<br> default:<br> log.Println("loading...")<br> time.Sleep(time.Second * 1)<br> }<br> }<br>}<br><br>func main() {<br> quit := make(chan int) // 退出通道<br><br> wg.Add(5)<br><br> go worker(quit) // work 1<br> go worker(quit) // work 2<br> go worker(quit) // work 3<br> go worker(quit) // work 4<br> go Done(quit) // 结束所有任务<br><br> wg.Wait()<br>}<br><br>func Done(ch chan int) {<br> defer wg.Done()<br><br> time.Sleep(time.Second * 10)<br> close(ch)<br>}<br><br>

这样吗?

//这里将有大量工作可能半个小时都未必执行完,这样 done 这个 channel 就无法收到退出信号
for i := 0; i < 100000; i++ {
fmt.Println(msg)
time.Sleep(time.Second)
}
------
只能将上面这段长时间执行的操作分解,并在步骤间判断是否需要退出。上面已经有朋友讲过了

楼上说 context 的真的认真审题了吗…

不管是自定义的 done 还是用 context 包,必须在某个位置读取信号,我没见过从外部强制销毁 goroutine 的办法

context 只不过是简化了之前传入控制 channel 的方法,如果底层没有 等待+检查+执行的机制,开弓没有回头箭

你这等于要在外部强制 kill -9 杀掉这个 goroutine 的效果?
考虑通过退出主协程的方式强制让子协程被杀死这种方式吗.
比方主协程开启 workRoutine 子协程以后,监听信号,当你输入信号时,主协会直接退出,同时子协程也被退出

沒有辦法直接關閉 只能提醒可以關閉 直到協程提起接受關閉才可以 這裡涉及一部分操作系統線程排程問題很複雜

应该是你的代码写的有点问题:

<br>package main<br><br>import (<br> "fmt"<br> "sync"<br> "time"<br>)<br><br>func workRoutine(done chan string, wg *sync.WaitGroup) {<br> work := func() &lt;-chan string{<br> c := make(chan string, 1)<br> msg := "work"<br> go func() {<br> for i := 0; i &lt; 100000; i++ {<br> fmt.Println(msg)<br> time.Sleep(time.Second)<br> }<br> }()<br> c &lt;- msg<br> return c<br> }<br> select {<br> case donemsg := &lt;-done:<br> fmt.Println(donemsg)<br> case msg := &lt;-work():<br> fmt.Println(msg)<br> case &lt;-time.After(time.Second * 1000):<br> fmt.Println("timeout")<br> }<br> wg.Done()<br>}<br><br>func closeWorkRoutine(done chan string, wg *sync.WaitGroup) {<br> time.Sleep(time.Second * 10)<br> done &lt;- "done"<br> wg.Done()<br>}<br><br>func main() {<br> done := make(chan string, 1)<br> wg := sync.WaitGroup{}<br> wg.Add(2)<br> go workRoutine(done, &amp;wg)<br> go closeWorkRoutine(done, &amp;wg)<br> wg.Wait()<br>}<br>

实际上 goroutine 是不建议暴露 ID 和外部改变 goroutine 的行为的,所以你不能像线程那样去控制它。

我基于你的代码,改动的这个例子,是把死循环的 work 逻辑移动到了一个单独的 goroutine 中,这样 select 对应的其他 case 可以触发。如果死循环 work 它确实无法退出,我们最终是通过 main 函数退出来终止它的运行的,也就是进程退出。

如果你的核心计算无法插入中断 /判断代码,那没有任何办法实现你的需求只能想其他方法
其实你的问题描述不准确,应该改为如何(从外部)停止一个协程,目前是不支持的,协程只能内部关闭无法外部中断
考虑下子进程方向?反正你要强制关闭那直接 kill 也实现差不多的效果。。

赞同楼上,每个任务开始时检查是否有退出事件

我不记得是哪里看到说 go 的设计逻辑对于这个问题的回应是:

用户需要自己处理好退出的机制,通过外面方式去强制结束一个运行的携程是不安全的。

隔一段代码监测一下是否被设置了退出标志位。这是最简单的思路。重庆 IT-golang 群:186 8029 2450 一起学习,一起成长。

楼上没一个认真看问题的…

只能单独封装然后 exec 运行子进程然后杀掉了,因为前几年刚写 Golang 的时候也想过,后面发现 golang 的设计就是要一层一层优雅退出…如果那部分代码不是自己能控制的就只能另外开进程然后 ipc 或者 args 传任务参数过去然后通过 ipc 或者 stdout 获取返回信息…如果想要中途停止 exec 有个 kill 函数,直接杀掉就行…

#22 按您所说的实现可以直接简化成这样了

<br>func workRoutine(wg *sync.WaitGroup) {<br> go func() {<br> go func() {<br> for i := 0; i &lt; 100000; i++ {<br> fmt.Println("do something")<br> time.Sleep(time.Second)<br> }<br> }()<br> }()<br> wg.Done()<br>}<br><br>func closeWorkRoutine(wg *sync.WaitGroup) {<br> time.Sleep(time.Second * 10)<br> wg.Done()<br>}<br><br>func main() {<br> wg := sync.WaitGroup{}<br> wg.Add(2)<br> go workRoutine(&amp;wg)<br> go closeWorkRoutine(&amp;wg)<br> wg.Wait()<br>}<br>

没法关闭,只能给 goroutine 发信号,goroutine 自己返回

如果你不检查退出条件,线程也没办法强杀的

强制退出会导致中间状态 /污染, 需要写 recovery 的逻辑.
你可以在每一个 loop 中判断 close, 或者用一个专门的 process 去做你的 work, 收到 close 的信号直接 os.exit

不过还是建议用 hadoop 或者 spark 之流去做你的 work, 不要用 kill 的方式退出程序.

楼主如果一定要在某个地方做非常耗时的操作,并且这个操作不太方便重新改逻辑嵌入 context 或者设置标识位去控制的话,建议直接把这部分逻辑摘出来,用 exec 族函数拉起来一个进程去执行它,然后在需要的时候 kill 掉它,让操作系统去干这个事情。

go 里面协程不都是独立的吗,怎么还有主协程、子协程一说

正常来说应该是通过层层传递 ctx 来分解任务,避免你说的 「 work 过于繁重,永远轮不到执行关闭的时候」,就像楼上说的那样。

如果确实做不到的话,试试下面这种方式:

https://paste.org.cn/WYbBsPUBWn

main goroutine 有点特殊的,可以参考这个
https://learnku.com/articles/41728
(3) go func () 调度流程

这域名有点厉害
work 再繁重的那部分,应该也能再分解成更小的功能吧,理论上不管啥操作,拆解到最最最小的那个操作,就不可能耗时很久了吧,然后每个都判断下 context 是否退出。

因为需要贴代码临时找了个支持的地方,域名与我无关……计算任务不一定能拆解,而且还要考虑可能是集成其他人的代码,这种情况大概还是会发生的。

在Go语言中,直接关闭或终止一个正在运行的协程(goroutine)并不像在其他某些编程语言中那样直接或简单。Go的设计哲学倾向于通过通信来共享内存(CSP模型),而不是直接控制线程或协程的生命周期。

要“关闭”一个协程,通常的做法是通过channel发送信号给该协程,让其在接收到信号后自行退出。例如,你可以创建一个停止信号channel,当需要关闭协程时,向该channel发送一个信号(如一个空结构体或布尔值)。协程内部则监听这个channel,一旦接收到信号就执行清理操作并退出。

以下是一个简单的示例:

package main

import (
    "fmt"
    "time"
)

func worker(stopCh <-chan struct{}) {
    for {
        select {
        case <-stopCh:
            fmt.Println("Worker stopping")
            return
        default:
            fmt.Println("Worker working")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    stopCh := make(chan struct{})
    go worker(stopCh)
    time.Sleep(5 * time.Second)
    close(stopCh) // 注意:这里只是关闭channel,不发送值,实际使用中通常会发送一个信号值
}

注意,在上面的示例中,close(stopCh)只是关闭了channel,但更常见的做法是通过stopCh <- struct{}{}发送一个信号值来通知worker协程停止。直接关闭一个无缓冲的channel通常只用于通知接收方不会再有新的值发送。

回到顶部