mirror of
https://github.com/crazybber/awesome-patterns.git
synced 2024-11-24 21:46:03 +03:00
96 lines
3.2 KiB
Go
96 lines
3.2 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/davecgh/go-spew/spew"
|
||
)
|
||
|
||
// The goroutine has a few paths to termination:
|
||
// • When it has completed its work.
|
||
// • When it cannot continue its work due to an unrecoverable error.
|
||
// • When it’s told to stop working
|
||
|
||
/**
|
||
We get the first two paths for free—these paths are your algorithm—but what about
|
||
work cancellation? This turns out to be the most important bit because of the net‐
|
||
work effect: if you’ve begun a goroutine, it’s most likely cooperating with several other
|
||
goroutines in some sort of organized fashion.
|
||
**/
|
||
|
||
func main() {
|
||
resourceLeak()
|
||
cancellationSignal()
|
||
}
|
||
|
||
// Here we see that the main goroutine passes a nil channel into doWork. Therefore, the
|
||
// strings channel will never actually gets any strings written onto it, and the goroutine
|
||
// containing doWork will remain in memory for the lifetime of this process (we would
|
||
// even deadlock if we joined the goroutine within doWork and the main goroutine).
|
||
// In this example, the lifetime of the process is very short, but in a real program, gorou‐
|
||
// tines could easily be started at the beginning of a long-lived program. In the worst
|
||
// case, the main goroutine could continue to spin up goroutines throughout its life,
|
||
// causing creep in memory utilization.
|
||
func resourceLeak() {
|
||
doWork := func(strings <-chan string) <-chan interface{} {
|
||
completed := make(chan interface{})
|
||
go func() {
|
||
defer fmt.Println("doWork exited.")
|
||
defer close(completed)
|
||
for s := range strings {
|
||
// Do something interesting
|
||
fmt.Println(s)
|
||
}
|
||
}()
|
||
return completed
|
||
}
|
||
doWork(nil)
|
||
// Perhaps more work is done here
|
||
fmt.Println("Done.")
|
||
}
|
||
|
||
// The way to successfully mitigate this is to establish a signal between the parent gorou‐
|
||
// tine and its children that allows the parent to signal cancellation to its children. By
|
||
// convention, this signal is usually a read-only channel named done. The parent gorou‐
|
||
// tine passes this channel to the child goroutine and then closes the channel when it
|
||
// wants to cancel the child goroutine. Here’s an example:
|
||
func cancellationSignal() {
|
||
// Here we pass the done channel to the doWork function. As a convention, this channel is the first parameter.
|
||
doWork := func(
|
||
done <-chan interface{},
|
||
strings <-chan string,
|
||
) <-chan interface{} {
|
||
terminated := make(chan interface{})
|
||
go func() {
|
||
defer fmt.Println("doWork exited.")
|
||
defer close(terminated)
|
||
for {
|
||
select {
|
||
case s := <-strings:
|
||
fmt.Println(s)
|
||
// On this line we see the ubiquitous for-select pattern in use. One of our case statements
|
||
// is checking whether our done channel has been signaled. If it has, we return from the goroutine.
|
||
case t := <-done:
|
||
spew.Dump(t)
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
return terminated
|
||
}
|
||
done := make(chan interface{})
|
||
terminated := doWork(done, nil)
|
||
// Here we create another goroutine that will cancel the goroutine spawned in
|
||
// doWork if more than one second passes.
|
||
go func() {
|
||
// Cancel the operation after 1 second.
|
||
time.Sleep(1 * time.Second)
|
||
fmt.Println("Canceling doWork goroutine...")
|
||
close(done)
|
||
}()
|
||
// This is where we join the goroutine spawned from doWork with the main goroutine.
|
||
<-terminated
|
||
fmt.Println("Done.")
|
||
}
|