【问题标题】:Golang pattern to kill multiple goroutines at onceGolang 模式一次杀死多个 goroutine
【发布时间】:2020-07-15 08:34:31
【问题描述】:

我有两个 goroutine,如下面的 sn-p 所示。我想同步它们,这样当一个返回时,另一个也应该退出。实现这一目标的最佳方法是什么?

func main() {

  go func() {
    ...
    if err != nil {
      return
    }
  }()

  go func() {
    ...
    if err != nil {
      return
    }
  }()


}

我在这里https://play.golang.org/p/IqawStXt7rt 模拟了这种情况,并尝试用一个通道来解决它,以表示例程已完成。这看起来可能会写入已关闭的通道,从而导致恐慌。解决此问题的最佳方法是什么?

【问题讨论】:

  • 使用两个通道,使用接近信号完成play.golang.org/p/FQauwB7KFpS
  • “交叉延迟关闭模式!?”很有意义。我想我错误地认为这可以通过一个渠道来解决。
  • @CeriseLimón 不会在这里订购已完成的事情吗?如果 done2 先返回怎么办?更改要缓冲大小为 1 的通道可能是一种解决方法。
  • 完成顺序无关紧要。 main 函数在打印消息并返回之前等待两个 goroutine 完成。

标签: go channel goroutine


【解决方案1】:

首先将等待go-routines和done频道分开。

使用sync.WaitGroup 来协调goroutines。

func main() {
    wait := &sync.WaitGroup{}
    N := 3

    wait.Add(N)
    for i := 1; i <= N; i++ {
        go goFunc(wait, i, true)
    }

    wait.Wait()
    fmt.Println(`Exiting main`)
}

每个 goroutine 将如下所示:

// code for the actual goroutine
func goFunc(wait *sync.WaitGroup, i int, closer bool) {
    defer wait.Done()
    defer fmt.Println(`Exiting `, i)

    T := time.Tick(time.Duration(100*i) * time.Millisecond)
    for {
        select {
        case <-T:
            fmt.Println(`Tick `, i)
            if closer {
                return
            }
        }
    }
}

(https://play.golang.org/p/mDO4P56lzBU)

我们的 main 函数在退出之前成功地等待 goroutines 退出。每个 goroutine 都在关闭自己,我们想要一种同时取消所有 goroutine 的方法。

我们将使用chan 来执行此操作,并利用此从频道接收的功能:

QUOTE:关闭通道上的接收操作总是可以立即进行,在接收到任何先前发送的值之后产生元素类型的零值。 (https://golang.org/ref/spec#Receive_operator)

我们修改 goroutine 以检查 CLOSE:

func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
    defer wait.Done()
    defer fmt.Println(`Exiting `, i)

    T := time.Tick(time.Duration(100*i) * time.Millisecond)
    for {
        select {
        case <-CLOSE:
            return
        case <-T:
            fmt.Println(`Tick `, i)
            if closer {
                close(CLOSE)
            }
        }
    }
}

然后我们更改 func main 以便它通过 CLOSE 通道,我们将设置 closer 变量以便只有最后一个 goroutine 会触发关闭:

func main() {
    wait := &sync.WaitGroup{}
    N := 3
    CLOSE := make(chan struct{})

    // Launch the goroutines
    wait.Add(N)
    for i := 1; i <= N; i++ {
        go goFunc(wait, i, i == N, CLOSE)
    }

    // Wait for the goroutines to finish
    wait.Wait()
    fmt.Println(`Exiting main`)
}

(https://play.golang.org/p/E91CtRAHDp2)

现在看起来一切正常。

但事实并非如此。 并发很难。 这段代码中潜伏着一个错误,正等着在生产中咬你。让我们浮出水面。

更改我们的示例,以便 每个 goroutine 将关闭:

func main() {
    wait := &sync.WaitGroup{}
    N := 3
    CLOSE := make(chan struct{})

    // Launch the goroutines
    wait.Add(N)
    for i := 1; i <= N; i++ {
        go goFunc(wait, i, true /*** EVERY GOROUTINE WILL CLOSE ***/, CLOSE)
    }

    // Wait for the goroutines to finish
    wait.Wait()
    fmt.Println(`Exiting main`)
}

更改 goroutine 以便在关闭之前需要一段时间。我们希望两个 goroutine 同时关闭:

// code for the actual goroutine
func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
    defer wait.Done()
    defer fmt.Println(`Exiting `, i)

    T := time.Tick(time.Duration(100*i) * time.Millisecond)
    for {
        select {
        case <-CLOSE:
            return
        case <-T:
            fmt.Println(`Tick `, i)
            if closer {
                /*** TAKE A WHILE BEFORE CLOSING ***/
                time.Sleep(time.Second)
                close(CLOSE)
            }
        }
    }
}


(https://play.golang.org/p/YHnbDpnJCks)

我们崩溃了:

Tick  1
Tick  2
Tick  3
Exiting  1
Exiting  2
panic: close of closed channel

goroutine 7 [running]:
main.goFunc(0x40e020, 0x2, 0x68601, 0x430080)
    /tmp/sandbox558886627/prog.go:24 +0x2e0
created by main.main
    /tmp/sandbox558886627/prog.go:38 +0xc0

Program exited: status 2.

虽然关闭通道上的接收立即返回,但您无法关闭关闭通道。

我们需要一点协调。我们可以使用sync.Mutexbool 来指示我们是否关闭了通道。让我们创建一个结构来执行此操作:

type Close struct {
    C chan struct{}
    l sync.Mutex
    closed bool
}

func NewClose() *Close {
    return &Close {
        C: make(chan struct{}),
    }
}

func (c *Close) Close() {
    c.l.Lock()
    if (!c.closed) {
        c.closed=true
        close(c.C)
    }
    c.l.Unlock()
}

重写我们的 gofunc 和我们的 main 以使用我们新的 Close 结构,我们很高兴: https://play.golang.org/p/eH3djHu8EXW

并发的问题在于,您总是需要想知道如果另一个“线程”在代码中的其他任何地方会发生什么。

【讨论】:

    【解决方案2】:

    您可以使用上下文在两个 go 例程之间进行通信。 例如,

    package main
    
    import (
        "context"
        "sync"
    )
    
    func main() {
    
        ctx, cancel := context.WithCancel(context.Background())
        wg := sync.WaitGroup{}
        wg.Add(3)
        go func() {
            defer wg.Done()
            for {
                select {
                // msg from other goroutine finish
                case <-ctx.Done():
                    // end
                }
            }
        }()
    
        go func() {
            defer wg.Done()
            for {
                select {
                // msg from other goroutine finish
                case <-ctx.Done():
                    // end
                }
            }
        }()
    
        go func() {
            defer wg.Done()
            // your operation
            // call cancel when this goroutine ends
            cancel()
        }()
        wg.Wait()
    }
    
    

    【讨论】:

    • 谢谢!我需要阅读更多关于上下文的信息。多次调用取消会导致恐慌吗?
    • 不,不会。如果你觉得它有帮助,你可以投票给这个答案。谢谢~
    • 您的回答非常有帮助,我赞成。然而,在这种情况下使用上下文似乎是一种反模式dave.cheney.net/2017/08/20/context-isnt-for-cancellation,这就是为什么我没有将其标记为答案。
    • @johne:您似乎误解了那篇博文。虽然 Dave 可能希望将来以某种方式将上下文和取消分开,但不可否认,上下文是目前 Go 中处理取消的方式。事实上,std 库几乎完全使用 Contexts 进行取消。以net 包中的用法为例,以及os/execnet/http 等。更具体地说,http.Request 中的 Cancel 频道声明:Deprecated: Set the Request's context with NewRequestWithContext instead.
    【解决方案3】:

    在通道上使用 close 表示完成。这允许多个 goroutine 通过在通道上接收来检查完成情况。

    每个 goroutine 使用一个通道来表示 goroutine 完成。

    done1 := make(chan struct{}) // closed when goroutine 1 returns
    done2 := make(chan struct{}) // closed when goroutine 2 returns
    
    go func() {
        defer close(done1)
    
        timer1 := time.NewTicker(1 * time.Second)
        defer timer1.Stop()
    
        timer2 := time.NewTicker(2 * time.Second)
        defer timer2.Stop()
    
        for {
            select {
            case <-done2:
                // The other goroutine returned.
                fmt.Println("done func 1")
                return
            case <-timer1.C:
                fmt.Println("timer1 func 1")
            case <-timer2.C:
                fmt.Println("timer2 func 1")
                return
            }
    
        }
    }()
    
    go func() {
        defer close(done2)
        for {
            select {
            case <-done1:
                // The other goroutine returned.
                fmt.Println("done func 2")
                return
            default:
                time.Sleep(3 * time.Second)
                fmt.Println("sleep done from func 2")
                return
            }
    
        }
    }()
    
    fmt.Println("waiting for goroutines to complete")
    
    // Wait for both goroutines to return. The order that
    // we wait here does not matter. 
    <-done1
    <-done2
    
    fmt.Println("all done")
    

    Run it on the playground.

    【讨论】:

      【解决方案4】:

      您的问题是您希望 DONE 通道上的 单个 发送被 多个 侦听器接收。您还需要考虑您的 goroutine 或您的 main func 是否接收到 done 频道上的发送。

      我建议你将等待 go-routines 和 done 频道分开。

      import `sync`
      
      // This code will wait for the two functions to complete before ending
      func main {
         var wait sync.WaitGroup
         wait.Add(2)
         go func() {
           defer wait.Done()
         }()
         go g() {
           defer wait.Done()
         }()
         wait.Wait()
      }
      

      现在,如何管理完成。好吧,解决方案是使用sync.Cond 并让每个 goroutine 运行自己的 goroutine 以等待 Cond。这是一个例子:

      package main
      
      import (
          `fmt`
          `sync`
          `time`
      )
      
      // WaitForIt wraps a Cond and a Mutex for a simpler API:
      // .WAIT() chan struct{} will return a channel that will be
      //   signalled when the WaitForIt is done.
      // .Done() will indicate that the WaitForIt is done.
      type WaitForIt struct {
          L *sync.Mutex
          Cond *sync.Cond
      }
      
      func NewWaitForIt() *WaitForIt {
          l := &sync.Mutex{}
          c := sync.NewCond(l)
          return &WaitForIt{ l, c }
      }
      
      // WAIT returns a chan that will be signalled when
      // the Cond is triggered.
      func (w *WaitForIt) WAIT() chan struct{} {
          D := make(chan struct{})
          go func() {
              w.L.Lock()
              defer w.L.Unlock()
              w.Cond.Wait()
              D <- struct{}{}
              close(D)
          }()
          return D
      }
      
      // Done indicates that the Cond should be triggered.
      func (w *WaitForIt) Done() {
          w.Cond.Broadcast()
      }
      
      // doneFunc launches the func f with a chan that will be signalled when the
      // func should stop. It also handles WaitGroup synchronization
      func doneFunc(wait *sync.WaitGroup, waitForIt *WaitForIt, f func(DONE chan struct{})) {
          defer wait.Done()
          f(waitForIt.WAIT())
      }
      
      func main() {
          // wait will coordinate all the goroutines at the level of main()
          // between themselves the waitForIt will do the coordination
          wait := &sync.WaitGroup{}
          // waitForIt indicates to the goroutines when they should shut
          waitForIt := NewWaitForIt()
      
          // goFunc generates each goroutine. Only the 3-second goroutine will 
          // shutdown all goroutines
          goFunc := func(seconds int) func(chan struct{}) {
              return func(DONE chan struct{}) {
                  // this is the actual code of each goroutine
                  // it makes a ticker for a number of seconds,
                  // and prints the seconds after the ticker elapses,
                  // or exits if DONE is triggered
                  timer := time.NewTicker(time.Duration(seconds) * time.Second)
                  defer timer.Stop()
                  for {
                      select {
                      case <- DONE:
                          return
                      case <- timer.C:
                          if (3==seconds) {
                              waitForIt.Done()
                              // Don't shutdown here - we'll shutdown
                              // when our DONE is signalled
                          }
                      }
                  }
              }
          }
          // launch 3 goroutines, each waiting on a shutdown signal
          for i:=1; i<=3; i++ {
              wait.Add(1)
              go doneFunc(wait, waitForIt, goFunc(i))
          }
          // wait for all the goroutines to complete, and we're done
          wait.Wait()
      }
      

      这是您使用 WaitForIt 实现的示例:https://play.golang.org/p/llphW73G1xE 请注意,我必须删除 WaitForIt.Done 中的 Lock() 调用。虽然文档说你可以持有锁,但它阻止了你的第二个 goroutine 完成。

      【讨论】:

      • 您准确地表达了我的问题,而且比我做得好得多。在你和 Cerise 的评论之间,我对如何解决这个问题有一个好主意。我在这里错过了什么吗?忽略等待两个 goroutine 完成的过程。或者在这种情况下是否需要使用 sync.Cond? play.golang.org/p/RVUgIIggTJ3
      • 我对它进行了更多测试,然后在关闭的通道上遇到了发送。我需要阅读sync.Cond(它让我头晕目眩,试图了解发生了什么),但这看起来是正确的答案。
      • 您不应该使用 WaitForIt 类进入封闭通道。 sync.Cond 确实有点棘手,但这正是您在这些情况下想要的:它是一个条件,具有多个 goroutine 等待一个条件的能力。 WAIT 频道的内容只是将条件转换为chan,以便您可以在select 中访问它。我会看看是否可以使用WaitForIt 和您的代码创建一个播放示例。
      【解决方案5】:
      package main
      
      import (
          "fmt"
          "sync"
          "time"
      )
      
      func func1(done chan struct{}, wg *sync.WaitGroup) {
          defer wg.Done()
          timer1 := time.NewTicker(1 * time.Second)
          timer2 := time.NewTicker(2 * time.Second)
          for {
              select {
              case <-timer1.C:
                  fmt.Println("timer1 func 1")
              case <-timer2.C:
                  // Ask GC to sweep the tickers timer1, timer2
                  // as goroutine should return
                  timer1.Stop()
                  timer2.Stop()
      
                  fmt.Println("timer2 func 1")
      
                  done <- struct{}{} // Signal the other goroutine to terminate
      
                  fmt.Println("sent done from func 1")
                  return
              case <-done:
                  // Ask GC to sweep the tickers timer1, timer2
                  // as goroutine should return
                  timer1.Stop()
                  timer2.Stop()
      
                  fmt.Println("done func 1")
                  return
      
              }
      
          }
      }
      
      func func2(done chan struct{}, wg *sync.WaitGroup) {
          defer wg.Done()
          timer3 := time.NewTicker(3 * time.Second)
          for {
              select {
              case <-timer3.C:
                  // Ask GC to sweep the tickers timer3
                  // as goroutine should return
                  timer3.Stop()
      
                  fmt.Println("timer3 func 2")
      
                  done <- struct{}{} // Signal the other goroutine to terminate
      
                  fmt.Println("sent done from func 2")
                  return
              case <-done:
                  // Ask GC to sweep the tickers timer3
                  // as goroutine should return
                  timer3.Stop()
                  fmt.Println("done func 2")
                  return
              }
      
          }
      }
      
      func main() {
          // Chan used for signalling between goroutines
          done := make(chan struct{})
      
          // WaitGroup
          wg := sync.WaitGroup{}
      
          wg.Add(2)
      
          // Spawn the goroutine for func1
          go func1(done, &wg)
          // Spawn the goroutine for func2
          go func2(done, &wg)
      
          fmt.Println("starting sleep")
      
          // Wait for the goroutines
          wg.Wait()
      
          // Wait for 15 seconds
          // If not required, please remove
          // the lines below
          time.Sleep(15 * time.Second)
          fmt.Println("waited 15 seconds")
      
      }
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-11-19
        • 1970-01-01
        • 1970-01-01
        • 2021-09-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多