V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
hkhk366
V2EX  ›  Go 编程语言

go 语言如何关闭正在运行的协程?谢谢

  •  
  •   hkhk366 · 2022-05-07 11:24:29 +08:00 · 5743 次点击
    这是一个创建于 691 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 我可以直接在外部强制关掉,请问我应该如何做呢,谢谢。

    第 1 条附言  ·  2022-05-07 12:07:24 +08:00
    其实那个 for 循环那么长只是一个例子,我现实中的应用是那个地方相当于一个死循环,因为计算量非常大,这样好像永远无法触发上下文切换,我正在阅读 context ,看看 context 是否已可以取消一个一直运行的 for 循环
    37 条回复    2022-05-11 23:55:16 +08:00
    rophie123
        1
    rophie123  
       2022-05-07 11:27:09 +08:00
    context
    gollwang
        2
    gollwang  
       2022-05-07 11:28:32 +08:00
    楼上正解
    codefever
        3
    codefever  
       2022-05-07 11:32:46 +08:00
    如果想中途 cancel 掉,可以使用 context.WithCancel
    rekulas
        4
    rekulas  
       2022-05-07 11:33:59 +08:00   ❤️ 3
    严格来说是没办法的,只有发生上下文切换的时候你才有机会执行退出逻辑,如果协程阻塞在某个操作你没有办法去关闭
    阻塞了 context 什么的都没用
    DollarKiller
        5
    DollarKiller  
       2022-05-07 11:36:30 +08:00
    关闭掉的,context 只是 channel 发一个通知
    mainjzb
        6
    mainjzb  
       2022-05-07 11:44:31 +08:00
    没办法
    hejw19970413
        7
    hejw19970413  
       2022-05-07 11:53:27 +08:00
    context 正解
    brader
        8
    brader  
       2022-05-07 11:59:39 +08:00
    添加事件机制? work 里面的循环,每干完一轮活,就检查是否有退出事件?
    hejw19970413
        9
    hejw19970413  
       2022-05-07 12:09:31 +08:00
    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, 10*time.Second)
    f1(c1)
    }
    }
    }
    func main() {
    c, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
    defer cancel()
    go workRoutine(c)
    select {}
    }

    不知道这样能不能满足你的要求
    FreeEx
        10
    FreeEx  
       2022-05-07 12:12:46 +08:00
    执行每一个子任务的时候加上时间限制,并且没执行完一次子任务就检查一下是否需要退出。
    wunonglin
        11
    wunonglin  
       2022-05-07 12:16:41 +08:00
    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")
    }
    }
    }

    这样的?
    ilylx2008
        12
    ilylx2008  
       2022-05-07 12:20:39 +08:00
    `
    for (i:=0;i<100000;i++){
    select {
    case donemsg := <-done:
    //stop
    default:
    //do something
    }
    }
    Jessun
        13
    Jessun  
       2022-05-07 12:21:01 +08:00   ❤️ 1
    context 要看怎么用,我的建议是将 context 继续向下传递。

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

    根据目前信息,就这个思路最简单了。
    NIYIKI
        14
    NIYIKI  
       2022-05-07 13:50:46 +08:00
    关闭不了的
    zhangfuguan
        15
    zhangfuguan  
       2022-05-07 14:15:08 +08:00
    ```go

    package main

    import (
    "log"
    "sync"
    "time"
    )

    var wg sync.WaitGroup

    func worker(quit <-chan int) {
    defer wg.Done()
    for {
    select {
    case <-quit:
    log.Printf("收到退出信号")
    return // 必须 return ,否则 goroutine 是不会结束的
    default:
    log.Println("loading...")
    time.Sleep(time.Second * 1)
    }
    }
    }

    func main() {
    quit := make(chan int) // 退出通道

    wg.Add(5)

    go worker(quit) // work 1
    go worker(quit) // work 2
    go worker(quit) // work 3
    go worker(quit) // work 4
    go Done(quit) // 结束所有任务

    wg.Wait()
    }

    func Done(ch chan int) {
    defer wg.Done()

    time.Sleep(time.Second * 10)
    close(ch)
    }

    ```

    这样吗?
    walleL
        16
    walleL  
       2022-05-07 14:41:18 +08:00
    //这里将有大量工作可能半个小时都未必执行完,这样 done 这个 channel 就无法收到退出信号
    for i := 0; i < 100000; i++ {
    fmt.Println(msg)
    time.Sleep(time.Second)
    }
    ------
    只能将上面这段长时间执行的操作分解,并在步骤间判断是否需要退出。上面已经有朋友讲过了
    keepeye
        17
    keepeye  
       2022-05-07 14:58:24 +08:00   ❤️ 1
    楼上说 context 的真的认真审题了吗...

    不管是自定义的 done 还是用 context 包,必须在某个位置读取信号,我没见过从外部强制销毁 goroutine 的办法
    fighterlyt
        18
    fighterlyt  
       2022-05-07 15:39:35 +08:00   ❤️ 6
    context 只不过是简化了之前传入控制 channel 的方法,如果底层没有 等待+检查+执行的机制,开弓没有回头箭
    tianyou666shen
        19
    tianyou666shen  
       2022-05-07 15:48:52 +08:00
    你这等于要在外部强制 kill -9 杀掉这个 goroutine 的效果?
    考虑通过退出主协程的方式强制让子协程被杀死这种方式吗.
    比方主协程开启 workRoutine 子协程以后,监听信号,当你输入信号时,主协会直接退出,同时子协程也被退出
    stevefan1999
        20
    stevefan1999  
       2022-05-07 15:49:14 +08:00 via Android
    沒有辦法直接關閉 只能提醒可以關閉 直到協程提起接受關閉才可以 這裡涉及一部分操作系統線程排程問題很複雜
    stach
        21
    stach  
       2022-05-07 15:52:15 +08:00
    应该是你的代码写的有点问题:

    ```
    package main

    import (
    "fmt"
    "sync"
    "time"
    )

    func workRoutine(done chan string, wg *sync.WaitGroup) {
    work := func() <-chan string{
    c := make(chan string, 1)
    msg := "work"
    go func() {
    for i := 0; i < 100000; i++ {
    fmt.Println(msg)
    time.Sleep(time.Second)
    }
    }()
    c <- msg
    return c
    }
    select {
    case donemsg := <-done:
    fmt.Println(donemsg)
    case msg := <-work():
    fmt.Println(msg)
    case <-time.After(time.Second * 1000):
    fmt.Println("timeout")
    }
    wg.Done()
    }

    func closeWorkRoutine(done chan string, wg *sync.WaitGroup) {
    time.Sleep(time.Second * 10)
    done <- "done"
    wg.Done()
    }

    func main() {
    done := make(chan string, 1)
    wg := sync.WaitGroup{}
    wg.Add(2)
    go workRoutine(done, &wg)
    go closeWorkRoutine(done, &wg)
    wg.Wait()
    }
    ```
    stach
        22
    stach  
       2022-05-07 15:59:44 +08:00
    实际上 goroutine 是不建议暴露 ID 和外部改变 goroutine 的行为的,所以你不能像线程那样去控制它。

    我基于你的代码,改动的这个例子,是把死循环的 work 逻辑移动到了一个单独的 goroutine 中,这样 select 对应的其他 case 可以触发。如果死循环 work 它确实无法退出,我们最终是通过 main 函数退出来终止它的运行的,也就是进程退出。
    rekulas
        23
    rekulas  
       2022-05-07 16:08:31 +08:00
    如果你的核心计算无法插入中断 /判断代码,那没有任何办法实现你的需求只能想其他方法
    其实你的问题描述不准确,应该改为如何(从外部)停止一个协程,目前是不支持的,协程只能内部关闭无法外部中断
    考虑下子进程方向?反正你要强制关闭那直接 kill 也实现差不多的效果。。
    yeyypp92
        24
    yeyypp92  
       2022-05-07 17:38:55 +08:00
    赞同楼上,每个任务开始时检查是否有退出事件
    george404
        25
    george404  
       2022-05-07 17:42:02 +08:00
    我不记得是哪里看到说 go 的设计逻辑对于这个问题的回应是:

    用户需要自己处理好退出的机制,通过外面方式去强制结束一个运行的携程是不安全的。
    shanks02
        26
    shanks02  
       2022-05-07 17:59:03 +08:00
    隔一段代码监测一下是否被设置了退出标志位。这是最简单的思路。重庆 IT-golang 群:186 8029 2450 一起学习,一起成长。
    ClarkAbe
        27
    ClarkAbe  
       2022-05-07 19:10:54 +08:00
    楼上没一个认真看问题的.....

    只能单独封装然后 exec 运行子进程然后杀掉了,因为前几年刚写 Golang 的时候也想过,后面发现 golang 的设计就是要一层一层优雅退出....如果那部分代码不是自己能控制的就只能另外开进程然后 ipc 或者 args 传任务参数过去然后通过 ipc 或者 stdout 获取返回信息....如果想要中途停止 exec 有个 kill 函数,直接杀掉就行.....
    fenghuang
        28
    fenghuang  
       2022-05-07 19:24:17 +08:00
    @stach #22 按您所说的实现可以直接简化成这样了

    ```
    func workRoutine(wg *sync.WaitGroup) {
    go func() {
    go func() {
    for i := 0; i < 100000; i++ {
    fmt.Println("do something")
    time.Sleep(time.Second)
    }
    }()
    }()
    wg.Done()
    }

    func closeWorkRoutine(wg *sync.WaitGroup) {
    time.Sleep(time.Second * 10)
    wg.Done()
    }

    func main() {
    wg := sync.WaitGroup{}
    wg.Add(2)
    go workRoutine(&wg)
    go closeWorkRoutine(&wg)
    wg.Wait()
    }
    ```
    jeffh
        29
    jeffh  
       2022-05-07 19:26:03 +08:00
    没法关闭,只能给 goroutine 发信号,goroutine 自己返回
    lakehylia
        30
    lakehylia  
       2022-05-07 19:38:54 +08:00
    如果你不检查退出条件,线程也没办法强杀的
    joetse
        31
    joetse  
       2022-05-07 21:07:17 +08:00
    强制退出会导致中间状态 /污染, 需要写 recovery 的逻辑.
    你可以在每一个 loop 中判断 close, 或者用一个专门的 process 去做你的 work, 收到 close 的信号直接 os.exit

    不过还是建议用 hadoop 或者 spark 之流去做你的 work, 不要用 kill 的方式退出程序.
    bugfan
        32
    bugfan  
       2022-05-08 01:44:39 +08:00
    楼主如果一定要在某个地方做非常耗时的操作,并且这个操作不太方便重新改逻辑嵌入 context 或者设置标识位去控制的话,建议直接把这部分逻辑摘出来,用 exec 族函数拉起来一个进程去执行它,然后在需要的时候 kill 掉它,让操作系统去干这个事情。
    zaunist
        33
    zaunist  
       2022-05-08 17:12:23 +08:00
    @tianyou666shen go 里面协程不都是独立的吗,怎么还有主协程、子协程一说
    lanlanye
        34
    lanlanye  
       2022-05-08 20:56:30 +08:00
    正常来说应该是通过层层传递 ctx 来分解任务,避免你说的 「 work 过于繁重,永远轮不到执行关闭的时候」,就像楼上说的那样。

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

    https://paste.org.cn/WYbBsPUBWn
    tianyou666shen
        35
    tianyou666shen  
       2022-05-09 09:53:44 +08:00
    @zaunist main goroutine 有点特殊的,可以参考这个
    https://learnku.com/articles/41728
    (3) go func () 调度流程
    ob
        36
    ob  
       2022-05-10 20:34:17 +08:00
    @lanlanye 这域名有点厉害
    work 再繁重的那部分,应该也能再分解成更小的功能吧,理论上不管啥操作,拆解到最最最小的那个操作,就不可能耗时很久了吧,然后每个都判断下 context 是否退出。
    lanlanye
        37
    lanlanye  
       2022-05-11 23:55:16 +08:00
    @ob 因为需要贴代码临时找了个支持的地方,域名与我无关……计算任务不一定能拆解,而且还要考虑可能是集成其他人的代码,这种情况大概还是会发生的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   947 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 21:07 · PVG 05:07 · LAX 14:07 · JFK 17:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.