生成器模式
0x00 前言
Golang作为天生支持并发的语言,本系列将介绍Go的并发范式,也会提供部分示例代码,生产中依托业务稍加修改即可。
0x01 带缓冲的生成器
代码示例如下所示
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// 创建数据,每创建一个暂停1秒
func generateIntA() chan int {
ch := make(chan int, 10)
go func() {
for {
ch <- rand.Int()
time.Sleep(time.Second)
}
}()
return ch
}
// 不间断读取管道中数据
func getIntA(ch chan int) {
go func() {
for {
fmt.Println(<-ch)
}
}()
}
func main() {
ch := generateIntA()
getIntA(ch)
// 睡眠五秒,使得协程能输出结果
time.Sleep(time.Second * 5)
// 打印当前存在的goroutine数
fmt.Printf("runtime.NumGoroutine(): %v\n", runtime.NumGoroutine())
}
验证代码
package main
import (
"sync"
"sync/atomic"
"testing"
)
func BenchmarkGenerateIntA(b *testing.B) {
for i := 0; i < b.N; i++ {
c := make(chan uint32, 100)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := uint32(0); i < 1000; i++ {
c <- i % 2
}
close(c)
}()
var total uint32
for w := 0; w < 5; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
v, ok := <-c
if !ok {
break
}
atomic.AddUint32(&total, v)
}
}()
}
wg.Wait()
}
}
func BenchmarkGenerateIntB(b *testing.B) {
for i := 0; i < b.N; i++ {
c := make(chan uint32, 1)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := uint32(0); i < 1000; i++ {
c <- i % 2
}
close(c)
}()
var total uint32
for w := 0; w < 5; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
v, ok := <-c
if !ok {
break
}
atomic.AddUint32(&total, v)
}
}()
}
wg.Wait()
}
}
得到结果
goos: windows
goarch: amd64
pkg: exp5
cpu: Intel(R) Core(TM) i5-10500 CPU @ 3.10GHz
BenchmarkGenerateIntA-12 9722 125295 ns/op 542 B/op 3 allocs/op
BenchmarkGenerateIntB-12 3084 381594 ns/op 129 B/op 3 allocs/op
PASS
结论
可以看出平均处理时间,有缓冲区会远快于无缓冲区
优点:消费者先获取缓冲区数据,只有当缓冲区数据全部被读取后才会进入读取阻塞状态
缺点:缓冲区不可能无限,如果并发达到一定数量仍会进入阻塞,生产者生产速度赶不上消费速度
0x02 多个goroutine增强型生成器
示例代码如下所示
// 多个goroutine增强型生成器
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// 创建数据A
func generateIntA() chan int {
ch := make(chan int)
go func() {
for {
ch <- rand.Int()
}
}()
return ch
}
// 创建数据B
func generateIntB() chan int {
ch := make(chan int)
go func() {
for {
ch <- rand.Int()
}
}()
return ch
}
// 增强型生成器,每一秒生成一个新的AB生成器
func generateInt() chan int {
ch := make(chan int)
go func() {
for {
select {
case ch <- <-generateIntA():
time.Sleep(time.Second)
case ch <- <-generateIntB():
time.Sleep(time.Second)
}
}
}()
return ch
}
// 读取管道中数据
func getIntA(ch chan int) {
go func() {
for {
<-ch
// fmt.Println(<-ch)
}
}()
}
func main() {
ch := generateInt()
getIntA(ch)
// 等待五秒使协程能够运行
time.Sleep(time.Second * 5)
fmt.Printf("runtime.NumGoroutine(): %v\n", runtime.NumGoroutine())
}
基准测试结果如下
goos: windows
goarch: amd64
pkg: exp6
cpu: Intel(R) Core(TM) i5-10500 CPU @ 3.10GHz
BenchmarkGenerateInt-12 34491 36177 ns/op 7811 B/op 32 allocs/op
BenchmarkGenerateIntA-12 10000 671327 ns/op 24202 B/op 112 allocs/op
PASS
结论
优点:将阻塞状态出现的可能性降得更低,基本不会出现阻塞状态
缺点:生成大量goroutine,可能造成程序崩溃
0x03 含退出通知的生成器
代码示例如下所示
在此代码中,done通道作为退出通知的接收通道,利用对已关闭通道可以读取而不会引发panic的特性,不对done进行写入,当需要取消生成时,关闭done通道,select选择器可以运行读取done通道操作,从而退出生成器,结束协程。
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// 创建数据,每创建一个暂停1秒
func generateIntA(done chan int) chan int {
ch := make(chan int, 10)
go func() {
Label:
for {
select {
case ch <- rand.Int():
time.Sleep(time.Second)
case <-done:
break Label
}
}
close(ch)
}()
return ch
}
// 不间断读取管道中数据
func getIntA(ch, done chan int) {
go func() {
Label:
for {
select {
case <-ch:
fmt.Println(<-ch)
case <-done:
break Label
}
}
}()
}
func main() {
done := make(chan int)
ch := generateIntA(done)
getIntA(ch, done)
// 睡眠五秒,使得协程能输出结果
time.Sleep(time.Second * 5)
// 发送关闭信号,停止生成
close(done)
// 睡眠五秒保证协程退出
time.Sleep(time.Second * 5)
// 打印当前存在的goroutine数
fmt.Printf("runtime.NumGoroutine(): %v\n", runtime.NumGoroutine())
}
结论
优点:可以使生成器自动退出,减少资源消耗
0x04 融合并发、缓冲、退出通知的生成器
代码示例如下所示
// 多个goroutine增强型生成器 含推出通知机制
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
// 创建数据A
func generateIntA(done chan struct{}) chan int {
ch := make(chan int, 10)
go func() {
Label:
for {
select {
case ch <- rand.Int():
time.Sleep(time.Second)
case <-done:
break Label
}
}
close(ch)
}()
return ch
}
// 创建数据B
func generateIntB(done chan struct{}) chan int {
ch := make(chan int, 10)
go func() {
Label:
for {
select {
case ch <- rand.Int():
time.Sleep(time.Second)
case <-done:
break Label
}
}
close(ch)
}()
return ch
}
// 增强型生成器
func generateInt(done chan struct{}) chan int {
ch := make(chan int, 10)
go func() {
Label:
for {
select {
case ch <- <-generateIntA(done):
case ch <- <-generateIntB(done):
case <-done:
break Label
}
}
close(ch)
}()
return ch
}
// 读取管道中数据
func getIntA(ch chan int, done chan struct{}) {
go func() {
Label:
for {
select {
case <-ch:
fmt.Println(<-ch)
time.Sleep(time.Second)
case <-done:
break Label
}
}
}()
}
func main() {
// 规定监控通道
done := make(chan struct{})
ch := generateInt(done)
getIntA(ch, done)
// 等待五秒使协程能够运行
time.Sleep(time.Second * 5)
close(done)
// 等待十五秒使协程完全退出
time.Sleep(time.Second * 15)
fmt.Printf("runtime.NumGoroutine(): %v\n", runtime.NumGoroutine())
}
结论
性能最佳,并且可以自动退出
0x05 总结
- 合理的缓冲区大小可以有效提升程序运行效率。
- 使用增强型生成器时要注意控制程序的goroutine数量。
- 自动退出机制根据具体使用场景使用如生成大批订单号时,可以临时调用生成完订单号后退出,资源临时调用。