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

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

  •  
  •   hkhk366 · 56 天前 · 3527 次点击
    这是一个创建于 56 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 条附言  ·  56 天前
    其实那个 for 循环那么长只是一个例子,我现实中的应用是那个地方相当于一个死循环,因为计算量非常大,这样好像永远无法触发上下文切换,我正在阅读 context ,看看 context 是否已可以取消一个一直运行的 for 循环
    37 条回复    2022-05-11 23:55:16 +08:00
    rophie123
        1
    rophie123  
       56 天前
    context
    gollwang
        2
    gollwang  
       56 天前
    楼上正解
    codefever
        3
    codefever  
       56 天前
    如果想中途 cancel 掉,可以使用 context.WithCancel
    rekulas
        4
    rekulas  
       56 天前   ❤️ 3
    严格来说是没办法的,只有发生上下文切换的时候你才有机会执行退出逻辑,如果协程阻塞在某个操作你没有办法去关闭
    阻塞了 context 什么的都没用
    DollarKiller
        5
    DollarKiller  
       56 天前
    关闭掉的,context 只是 channel 发一个通知
    mainjzb
        6
    mainjzb  
       56 天前
    没办法
    hejw19970413
        7
    hejw19970413  
       56 天前
    context 正解
    brader
        8
    brader  
       56 天前
    添加事件机制? work 里面的循环,每干完一轮活,就检查是否有退出事件?
    hejw19970413
        9
    hejw19970413  
       56 天前
    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  
       56 天前
    执行每一个子任务的时候加上时间限制,并且没执行完一次子任务就检查一下是否需要退出。
    wunonglin
        11
    wunonglin  
       56 天前
    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  
       56 天前
    `
    for (i:=0;i<100000;i++){
    select {
    case donemsg := <-done:
    //stop
    default:
    //do something
    }
    }
    Jessun
        13
    Jessun  
       56 天前   ❤️ 1
    context 要看怎么用,我的建议是将 context 继续向下传递。

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

    根据目前信息,就这个思路最简单了。
    NIYIKI
        14
    NIYIKI  
       56 天前
    关闭不了的
    zhangfuguan
        15
    zhangfuguan  
       56 天前
    ```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  
       56 天前
    //这里将有大量工作可能半个小时都未必执行完,这样 done 这个 channel 就无法收到退出信号
    for i := 0; i < 100000; i++ {
    fmt.Println(msg)
    time.Sleep(time.Second)
    }
    ------
    只能将上面这段长时间执行的操作分解,并在步骤间判断是否需要退出。上面已经有朋友讲过了
    keepeye
        17
    keepeye  
       56 天前   ❤️ 1
    楼上说 context 的真的认真审题了吗...

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

    ```
    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  
       56 天前
    实际上 goroutine 是不建议暴露 ID 和外部改变 goroutine 的行为的,所以你不能像线程那样去控制它。

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

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

    只能单独封装然后 exec 运行子进程然后杀掉了,因为前几年刚写 Golang 的时候也想过,后面发现 golang 的设计就是要一层一层优雅退出....如果那部分代码不是自己能控制的就只能另外开进程然后 ipc 或者 args 传任务参数过去然后通过 ipc 或者 stdout 获取返回信息....如果想要中途停止 exec 有个 kill 函数,直接杀掉就行.....
    fenghuang
        28
    fenghuang  
       56 天前
    @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  
       56 天前
    没法关闭,只能给 goroutine 发信号,goroutine 自己返回
    lakehylia
        30
    lakehylia  
       56 天前
    如果你不检查退出条件,线程也没办法强杀的
    joetse
        31
    joetse  
       56 天前
    强制退出会导致中间状态 /污染, 需要写 recovery 的逻辑.
    你可以在每一个 loop 中判断 close, 或者用一个专门的 process 去做你的 work, 收到 close 的信号直接 os.exit

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

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

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