该模式用于处理数据限制问题,类似于生产者和消费者模式。使用channel
的方式通过共享信息的方式进行。有一个协程专门负责生产,另外一个协程负责接收数据。代码中使用随机的时间模拟实际情况中耗时部分。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
chanOwner := func(n int) <-chan int {
results := make(chan int, 5)
go func() {
defer close(results)
for i := 0; i < n; i++ {
results <- i
fmt.Printf("produce %d\n", i)
t := rand.Intn(2000)
time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
}
}()
return results
}
consumer := func(results <-chan int) {
for result := range results {
t := rand.Intn(2000)
time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
fmt.Printf("Received %d\n", result)
}
fmt.Println("Done receiving")
}
results := chanOwner(5)
consumer(results)
}
/*
代码输出:
produce 0
produce 1
Received 0
produce 2
Received 1
produce 3
produce 4
Received 2
Received 3
Received 4
Done receiving
*/
这种模式可以根据实际情况定义生产和消费的方式,不用担心出现数据竞争的问题。
最常规的模式:
for { // 死循环
select {
// 在这里采取有关操作
}
}
通过迭代的方式把数据写入channel
for _, s := range []string{"a", "b", "c"} {
select {
case <-done: // 这里是完成条件的标记
return
case stringStream <- s: // 在这里写入数据
}
}
死循环等待结束标记。
这种方式会尽可能早的结束工作,只要done
信号到达,立刻终止:
for {
select {
case <-done:
return
default:
}
// do something here
}
另一个等价方式
for {
select {
case <-done:
return
default:
// do something here
}
}
尽管goroutine
是一种轻量级的进程,而且一般不必担心使用太多的协程导致内存的问题,Go语言的有自动回收机制;但是,在某些情况下确实需要考虑出现某些协程一直无法回收的问题,这可还会引发一些其它的不良后果,给出下面的例子:
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
fmt.Println(s)
}
}()
return completed
}
doWork(nil)
fmt.Println("Done")
在上述的代码中,doWork
会永远阻塞,因为空的string channel不会有任何内容输出。本例子中的开销可能很小,但是在实际的工程中,这可能会引发大的问题。解决方法是通过父协程结束子协程。通过父协程给子协程发射终止的信号,使得子协程自动终止。
给出一般的操作方式:
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:
// do something
fmt.Println(s)
case <-done: // 如果关闭,则直接执行return
return
}
}
}()
return terminated
}
done := make(chan interface{})
terminated := doWork(done, nil) // 接收子协程的终止信号
go func() {
// cancel the operation after 1 second
time.Sleep(time.Second)
fmt.Println("Canceling doWork goroutine...")
close(done) // 关闭后相当于不存在阻塞的情况了。。。
}()
<-terminated // 在这里等待子协程的终止
fmt.Println("Done.")
上述代码是读数据的例子,下面给出写数据时发生协程泄露的例子:
package main
import (
"fmt"
"math/rand"
)
func main() {
newRandStream := func() <-chan int {
randStream := make(chan int)
go func() {
defer fmt.Println("newRandStream closure exited.")
defer close(randStream)
for {
randStream <- rand.Int()
}
}()
return randStream
}
randStream := newRandStream()
n := 3
fmt.Printf("%d random ints:\n", n)
for i := 0; i < n; i++ {
fmt.Printf("%d: %d\n", i, <-randStream) // 注意这种使用方式,也是合法的
}
}
/*
输出结果:
3 random ints:
0: 5577006791947779410
1: 8674665223082153551
2: 6129484611666145821
*/
上述代码中,randStream
始终没有结束,出现了协程泄露。。
改进方案:和写数据的方式类似,通过父协程给子协程发射结束信号即可。代码方案:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
newRandStream := func(done <-chan interface{}) <-chan int {
randStream := make(chan int)
go func() {
defer fmt.Println("newRandStream closure exited...")
defer close(randStream)
for {
select {
case randStream <- rand.Int():
case <-done:
return
}
}
}()
return randStream
}
done := make(chan interface{})
randStream := newRandStream(done)
n := 3
fmt.Printf("%d random ints\n", n)
for i := 0; i < 3; i++ {
fmt.Printf("%d:%d\n", n, <-randStream)
}
close(done)
// 等待同步看效果,不用等待也可以正常结束的,这里仅仅是为了显式说明一下
time.Sleep(time.Second)
}
/*
输出结果:
3 random ints
0:5577006791947779410
1:8674665223082153551
2:6129484611666145821
newRandStream closure exited...
*/
这种模式的方式是:把多个channel
的done
连接到一个done
上,如果这些channel
中任何至少一个关闭,则关闭这个done
。 代码中,如果出现一个任意一个协程结束,那么就出现终止信号。终止信号出现后,如果有协程没有结束,他们会继续执行,代码只是检测是否有协程终止,而不主动结束协程。
给出实例代码:
package main
import (
"fmt"
"time"
)
func main() {
// 从这里传入各个channel的done
var or func(channels ...<-chan interface{}) <-chan interface{}
or = func(channels ...<-chan interface{}) <-chan interface{} {
switch (len(channels)) {
case 0: // 递归结束的条件
return nil
case 1: // 只有一个直接返回
return channels[0]
}
orDone := make(chan interface{}) // 这是自己的标记
// 可以理解成一棵协程树,父节点需要孩子节点结束才能销毁。。。
// 在这里进行协程孩子节点的拓展,WTF好难理解。。。。。
// 在匿名函数中,如果一个channel的任何一个子channel结束,那么匿名函数的阻塞就会立刻结束,
// 之后会执行内部的defer操作,然后return一个关闭了的channel,相当于解除阻塞
go func() {
defer close(orDone) // 结束的时候释放本身的done信号
switch len(channels) {
case 2:
select {
case <-channels[0]:
case <-channels[1]:
}
default:
select {
// 如果case失败,则进行default,需要再判断一下,防止此次突然有结束的信号了
case <-channels[0]:
case <-channels[1]:
case <-channels[2]:
// 在这里追加父节点的协程终止信号,因为这是一棵或的树,只要有一个节点成功就可以释放掉
// 因此把父节点一起传入,只要有一个释放掉,父节点的channel就立刻进行释放......好机智的操作
// 这里追加自己的orDone,是为了`
case <-or(append(channels[3:], orDone)...): // 注意使用...符号
}
}
}()
return orDone
}
sig := func(after time.Duration) <-chan interface{} {
c := make(chan interface{})
go func() {
defer close(c) // 所在的goroutine结束后close,使用时间模拟工作时间
time.Sleep(after)
}()
return c
}
start := time.Now()
<-or(
sig(2*time.Hour),
sig(5*time.Minute),
sig(1*time.Second),
sig(1*time.Hour),
sig(1*time.Minute),
)
fmt.Printf("done after %v\n", time.Since(start))
}
/*
输出结果:
done after 1.000182106s
*/
从上述看出,仅仅执行到结果最短的那个,相当于一个“或”操作。代码采用了尾递归的方式,因为select
方式无法预判channel
的数量,而循环的方式需要处理大量的阻塞问题,不如尾递归的方式简洁。
上述代码最后的递归中,有一个地方不太理解:代码递归的过程中为什么要加入orDone
?希望有明白的同学可以解释一下!