简体   繁体   English

一次杀死多个 goroutines 的 Golang 模式

[英]Golang pattern to kill multiple goroutines at once

I have two goroutines as shown in the snippet below.我有两个 goroutine,如下面的代码片段所示。 I want to synchronize them such that when one returns, the other one should also exit.我想同步它们,这样当一个返回时,另一个也应该退出。 What is best way in go to achieve this? go 中实现此目标的最佳方法是什么?

func main() {

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

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


}

I have simulated this scenario here https://play.golang.org/p/IqawStXt7rt and tried to solve it with a channel to signal a routine is done.我在这里模拟了这种情况https://play.golang.org/p/IqawStXt7rt并尝试用一个通道来解决它,以表示例程已完成。 This looks like there can be a write to closed channel resulting in a panic.这看起来可能有写入关闭的通道导致恐慌。 What is the best way to solve this problem?解决这个问题的最佳方法是什么?

You can use context for communication between two go routines.您可以使用上下文在两个 go 例程之间进行通信。 For example,例如,

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()
}

Use close on a channel to signal completion.在通道上使用 close 表示完成。 This allows multiple goroutines to check for completion by receiving on the channel.这允许多个 goroutine 通过在通道上接收来检查完成情况。

Use one channel per goroutine to signal completion of the 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 .在操场上运行它

First separate the waiting on go-routines and the done channel.首先将等待 go-routines 和done通道分开。

Use a sync.WaitGroup to coordinate the goroutines.使用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`)
}

Each goroutine will look like this:每个 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 ) https://play.golang.org/p/mDO4P56lzBU

Our main func is successfully waiting for the goroutines to exit before it exits.我们的 main 函数在退出之前成功地等待 goroutines 退出。 Each goroutine is closing itself, and we want a way to cancel all our goroutines at the same time.每个 goroutine 都在关闭自己,我们想要一种同时取消所有 goroutine 的方法。

We'll do this with a chan , and make use of this feature of receiving from channels:我们将使用chan来执行此操作,并利用从频道接收的这一特性:

QUOTE: A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.引用:关闭通道上的接收操作总是可以立即进行,在接收到任何先前发送的值之后产生元素类型的零值。 ( https://golang.org/ref/spec#Receive_operator ) https://golang.org/ref/spec#Receive_operator

We modify our goroutines to check for a CLOSE:我们修改我们的 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)
            }
        }
    }
}

and then we change our func main so that it passes the CLOSE channel through, and we'll set the closer variable so that only the last of our goroutines will trigger the 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 ) https://play.golang.org/p/E91CtRAHDp2

Now it looks like everything is working.现在看起来一切正常。

But it isn't.但事实并非如此。 Concurrency is hard.并发很难。 There's a bug lurking in this code, just waiting to bite you in production.这段代码中潜伏着一个错误,正等着在生产中咬你。 Let's surface it.让我们浮出水面。

Change our example so that every goroutine will close:更改我们的示例,以便每个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`)
}

Change goroutine so that it takes a while before closing.更改 goroutine 以便在关闭之前需要一段时间。 We want two goroutines to be about to close at the same time:我们希望两个 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 ) https://play.golang.org/p/YHnbDpnJCks

We crash with:我们崩溃:

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.

While a receive on a closed channel returns immediately, you cannot close a closed channel.虽然关闭通道上的接收立即返回,但您无法关闭关闭的通道。

We need a little coordination.我们需要一点协调。 We can do this with a sync.Mutex and a bool to indicate whether we've closed the channel or not.我们可以使用sync.Mutexbool来指示我们是否关闭了通道。 Let's create a struct to do this:让我们创建一个结构来执行此操作:

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()
}

Rewrite our gofunc and our main to use our new Close struct, and we're good to go: https://play.golang.org/p/eH3djHu8EXW重写我们的 gofunc 和我们的 main 以使用我们的新 Close 结构,我们对 go 很好: https://play.golang.org/p/eH3djHu8EXW

The problem with concurrency is that you always need to be wondering what would happen if another 'thread' was anywhere else in the code.并发的问题在于,您总是需要想知道如果另一个“线程”在代码中的其他任何地方会发生什么。

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")

}

Your problem is that you want a single send on the DONE channel to be received by multiple listeners.您的问题是您希望 DONE 通道上的单个发送被多个侦听器接收。 You also need to consider whether a send on the done channel is received by your goroutines, or by your main func.您还需要考虑您的 goroutine 或您的main func 是否接收到done通道上的发送。

I suggest you rather separate the waiting on go-routines and the done channel.我建议您将等待 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()
}

Now, how to manage the Done.现在,如何管理完成。 Well, the solution is to use a sync.Cond and have each goroutine run its own goroutine to wait on the Cond.好吧,解决方案是使用sync.Cond并让每个 goroutine 运行自己的 goroutine 以等待 Cond。 Here's an example:这是一个例子:

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()
}

Here's your example implemented using WaitForIt: https://play.golang.org/p/llphW73G1xE Note that I had to remove the Lock() call in WaitForIt.Done .这是您使用 WaitForIt 实现的示例: https://play.golang.org/p/llphW73G1xE请注意,我必须删除WaitForIt.Done中的Lock()调用。 Although the documentation says you're allowed to hold the lock, it was blocking your 2nd goroutine from completing.尽管文档说您可以持有锁,但它阻止了您的第二个 goroutine 完成。

You could use channel closing pattern to wait within multiple go routines.您可以使用通道关闭模式在多个 go 例程中等待。

package main

import (
    "os"
    "os/signal"
    "sync"
    "syscall"
)


type RunGroup struct {
  sync.WaitGroup
}

// Run handles wait group state
func (runGroup *RunGroup) Run(f func()) {
  runGroup.Add(1)
  go func() {
    f()
    runGroup.Done()
  }()
}

func doStuff(done <-chan any, id int) {
  println("Doing something", id)
  <-done
  println("DONE", id)
}

func main() {
  // Done channel
  done := make(chan any)
  // Setup Shutdown listeners
  sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGTERM)
    signal.Notify(sigChan, syscall.SIGINT)
  go func() {
    rawSig := <-sigChan
    sig := rawSig.String()
    println("Caught signal, shutting down.", sig)
    close(done)
  }()

  runGroup := RunGroup{}
  // Do some stuff
  runGroup.Run(func () {
    doStuff(done, 1)
  })
  runGroup.Run(func () {
    doStuff(done, 2)
  })
  runGroup.Run(func () {
    doStuff(done, 3)
  })

  // Wait mainthread until interrupt
  runGroup.Wait()
}

Output Output

go run ./main.go
Doing something 3
Doing something 2
Doing something 1
^CCaught signal, shutting down. interrupt
DONE 3
DONE 1
DONE 2

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM