任务编排
这里的编排既指安排 goroutine 按照指定的顺序执行,也指多个 chan 按照指定的方式组合处理的方式。goroutine 的编排类似“击鼓传花”的例子,我们通过编排数据在 chan 之间的流转,就可以控制 goroutine 的执行。会重点介绍下多个 chan 的编排方式,总共 5 种,分别是 Or-Done 模式、扇入模式、扇出模式、Stream 和 MapReduce。
基本编排
基本处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package goroutine
import (
"fmt"
"github.com/abulo/ratel/logger"
"github.com/abulo/ratel/util"
"github.com/pkg/errors"
)
func try(fn func() error, cleaner func()) (ret error) {
if cleaner != nil {
defer cleaner()
}
defer func() {
if err := recover(); err != nil {
logger.Logger.Error("recover", err)
if _, ok := err.(error); ok {
ret = err.(error)
} else {
ret = fmt.Errorf("%+v", err)
}
ret = errors.Wrap(ret, fmt.Sprintf("%s", util.FunctionName(fn)))
}
}()
return fn()
}
func try2(fn func(), cleaner func()) (ret error) {
if cleaner != nil {
defer cleaner()
}
defer func() {
if err := recover(); err != nil {
logger.Logger.Error("recover", err)
if _, ok := err.(error); ok {
ret = err.(error)
} else {
ret = fmt.Errorf("%+v", err)
}
}
}()
fn()
return nil
}
|
安全并发:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
package goroutine
import (
"sync"
"time"
"github.com/abulo/ratel/logger"
"github.com/codegangsta/inject"
)
// Serial 串行
func Serial(fns ...func()) func() {
return func() {
for _, fn := range fns {
fn()
}
}
}
// Parallel 并发执行
func Parallel(fns ...func()) func() {
var wg sync.WaitGroup
return func() {
wg.Add(len(fns))
for _, fn := range fns {
go try2(fn, wg.Done)
}
wg.Wait()
}
}
// RestrictParallel 并发,最大并发量restrict
func RestrictParallel(restrict int, fns ...func()) func() {
var channel = make(chan struct{}, restrict)
return func() {
var wg sync.WaitGroup
for _, fn := range fns {
wg.Add(1)
go func(fn func()) {
defer wg.Done()
channel <- struct{}{}
try2(fn, nil)
<-channel
}(fn)
}
wg.Wait()
close(channel)
}
}
// GoDirect ...
func GoDirect(fn interface{}, args ...interface{}) {
var inj = inject.New()
for _, arg := range args {
inj.Map(arg)
}
go func() {
defer func() {
if err := recover(); err != nil {
logger.Logger.Error("recover", err)
}
}()
// 忽略返回值, goroutine执行的返回值通常都会忽略掉
_, err := inj.Invoke(fn)
if err != nil {
logger.Logger.Error("inject", err)
return
}
}()
}
// Go goroutine
func Go(fn func()) {
go try2(fn, nil)
}
// DelayGo goroutine
func DelayGo(delay time.Duration, fn func()) {
go func() {
defer func() {
if err := recover(); err != nil {
logger.Logger.Error("inject", err)
}
}()
time.Sleep(delay)
fn()
}()
}
// SafeGo safe go
func SafeGo(fn func(), rec func(error)) {
go func() {
err := try2(fn, nil)
if err != nil {
rec(err)
}
}()
}
|
顺序编排:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package goroutine
import (
"go.uber.org/multierr"
)
// SerialWithError ...
func SerialWithError(fns ...func() error) func() error {
return func() error {
var errs error
for _, fn := range fns {
errs = multierr.Append(errs, try(fn, nil))
}
return errs
}
}
// 创建一个迭代器
func SerialUntilError(fns ...func() error) func() error {
return func() error {
for _, fn := range fns {
if err := try(fn, nil); err != nil {
return err
// return errors.Wrap(err, xstring.FunctionName(fn))
}
}
return nil
}
}
// 策略注入
type WhenError int
var (
// ReturnWhenError ...
ReturnWhenError WhenError = 1
// ContinueWhenError ...
ContinueWhenError WhenError = 2
// PanicWhenError ...
PanicWhenError WhenError = 3
// LastErrorWhenError ...
LastErrorWhenError WhenError = 4
)
// SerialWhenError ...
func SerialWhenError(we WhenError) func(fn ...func() error) func() error {
return func(fns ...func() error) func() error {
return func() error {
var errs error
for _, fn := range fns {
if err := try(fn, nil); err != nil {
switch we {
case ReturnWhenError: // 直接退出
return err
case ContinueWhenError: // 继续执行
errs = multierr.Append(errs, err)
case PanicWhenError: // panic
panic(err)
case LastErrorWhenError: // 返回最后一个错误
errs = err
}
}
}
return errs
}
}
}
|
并发编排:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
package goroutine
import (
"sync"
"golang.org/x/sync/errgroup"
)
// ParallelWithError ...
func ParallelWithError(fns ...func() error) func() error {
return func() error {
eg := errgroup.Group{}
for _, fn := range fns {
eg.Go(fn)
}
return eg.Wait()
}
}
// ParallelWithErrorChan calls the passed functions in a goroutine, returns a chan of errors.
// fns会并发执行,chan error
func ParallelWithErrorChan(fns ...func() error) chan error {
total := len(fns)
errs := make(chan error, total)
var wg sync.WaitGroup
wg.Add(total)
go func(errs chan error) {
wg.Wait()
close(errs)
}(errs)
for _, fn := range fns {
go func(fn func() error, errs chan error) {
defer wg.Done()
errs <- try(fn, nil)
}(fn, errs)
}
return errs
}
// RestrictParallelWithErrorChan calls the passed functions in a goroutine, limiting the number of goroutines running at the same time,
// returns a chan of errors.
func RestrictParallelWithErrorChan(concurrency int, fns ...func() error) chan error {
total := len(fns)
if concurrency <= 0 {
concurrency = 1
}
if concurrency > total {
concurrency = total
}
var wg sync.WaitGroup
errs := make(chan error, total)
jobs := make(chan func() error, concurrency)
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
//consumer
go func(jobs chan func() error, errs chan error) {
defer wg.Done()
for fn := range jobs {
errs <- try(fn, nil)
}
}(jobs, errs)
}
go func(errs chan error) {
//producer
for _, fn := range fns {
jobs <- fn
}
close(jobs)
//wait for block errs
wg.Wait()
close(errs)
}(errs)
return errs
}
|
Or-Done 模式
首先来看 Or-Done 模式。Or-Done 模式是信号通知模式中更宽泛的一种模式。这里提到了“信号通知模式”,我先来解释一下。
我们会使用“信号通知”实现某个任务执行完成后的通知机制,在实现时,我们为这个任务定义一个类型为 chan struct{}
类型的 done 变量,等任务结束后,我们就可以 close 这个变量,然后,其它 receiver 就会收到这个通知。
这是有一个任务的情况,如果有多个任务,只要有任意一个任务执行完,我们就想获得这个信号,这就是 Or-Done 模式。
比如,你发送同一个请求到多个微服务节点,只要任意一个微服务节点返回结果,就算成功,这个时候,就可以参考下面的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package example
func or(channels ...<-chan interface{}) <-chan interface{} {
// 特殊情况,只有零个或者1个chan
switch len(channels) {
case 0:
return nil
case 1:
return channels[0]
}
orDone := make(chan interface{})
go func() {
defer close(orDone)
switch len(channels) {
case 2: // 2个也是一种特殊情况
select {
case <-channels[0]:
case <-channels[1]:
}
default: // 超过两个,二分法递归处理
m := len(channels) / 2
select {
case <-or(channels[:m]...):
case <-or(channels[m:]...):
}
}
}()
return orDone
}
|
我们可以写一个测试程序测试它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func sig(after time.Duration) <-chan interface{} {
c := make(chan interface{})
go func() {
defer close(c)
time.Sleep(after)
}()
return c
}
func main() {
start := time.Now()
<-or(
sig(10*time.Second),
sig(20*time.Second),
sig(30*time.Second),
sig(40*time.Second),
sig(50*time.Second),
sig(01*time.Minute),
)
fmt.Printf("done after %v", time.Since(start))
}
|
这里的实现使用了一个巧妙的方式,当 chan 的数量大于 2 时,使用递归的方式等待信号。
在 chan 数量比较多的情况下,递归并不是一个很好的解决方式,根据这一讲最开始介绍的反射的方法,我们也可以实现 Or-Done 模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func or(channels ...<-chan interface{}) <-chan interface{} {
//特殊情况,只有0个或者1个
switch len(channels) {
case 0:
return nil
case 1:
return channels[0]
}
orDone := make(chan interface{})
go func() {
defer close(orDone)
// 利用反射构建SelectCase
var cases []reflect.SelectCase
for _, c := range channels {
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(c),
})
}
// 随机选择一个可用的case
reflect.Select(cases)
}()
return orDone
}
|
这是递归和反射两种方法实现 Or-Done 模式的代码。反射方式避免了深层递归的情况,可以处理有大量 chan 的情况。其实最笨的一种方法就是为每一个 Channel 启动一个 goroutine,不过这会启动非常多的 goroutine,太多的 goroutine 会影响性能,所以不太常用。你只要知道这种用法就行了,不用重点掌握。
扇入模式
扇入借鉴了数字电路的概念,它定义了单个逻辑门能够接受的数字信号输入最大量的术语。一个逻辑门可以有多个输入,一个输出。
在软件工程中,模块的扇入是指有多少个上级模块调用它。而对于我们这里的 Channel 扇入模式来说,就是指有多个源 Channel 输入、一个目的 Channel 输出的情况。扇入比就是源 Channel 数量比 1。
每个源 Channel 的元素都会发送给目标 Channel,相当于目标 Channel 的 receiver 只需要监听目标 Channel,就可以接收所有发送给源 Channel 的数据。
扇入模式也可以使用反射、递归,或者是用最笨的每个 goroutine 处理一个 Channel 的方式来实现。
这里我列举下递归和反射的方式,帮你加深一下对这个技巧的理解。
反射的代码比较简短,易于理解,主要就是构造出 SelectCase slice,然后传递给 reflect.Select 语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func fanInReflect(chans ...<-chan interface{}) <-chan interface{} {
out := make(chan interface{})
go func() {
defer close(out)
// 构造SelectCase slice
var cases []reflect.SelectCase
for _, c := range chans {
cases = append(cases, reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(c),
})
}
// 循环,从cases中选择一个可用的
for len(cases) > 0 {
i, v, ok := reflect.Select(cases)
if !ok { // 此channel已经close
cases = append(cases[:i], cases[i+1:]...)
continue
}
out <- v.Interface()
}
}()
return out
}
|
递归模式也是在 Channel 大于 2 时,采用二分法递归 merge。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func fanInRec(chans ...<-chan interface{}) <-chan interface{} {
switch len(chans) {
case 0:
c := make(chan interface{})
close(c)
return c
case 1:
return chans[0]
case 2:
return mergeTwo(chans[0], chans[1])
default:
m := len(chans) / 2
return mergeTwo(
fanInRec(chans[:m]...),
fanInRec(chans[m:]...))
}
}
|
这里有一个 mergeTwo 的方法,是将两个 Channel 合并成一个 Channel,是扇入形式的一种特例(只处理两个 Channel)。 下面我来借助一段代码帮你理解下这个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func mergeTwo(a, b <-chan interface{}) <-chan interface{} {
c := make(chan interface{})
go func() {
defer close(c)
for a != nil || b != nil { //只要还有可读的chan
select {
case v, ok := <-a:
if !ok { // a 已关闭,设置为nil
a = nil
continue
}
c <- v
case v, ok := <-b:
if !ok { // b 已关闭,设置为nil
b = nil
continue
}
c <- v
}
}
}()
return c
}
|
扇出模式
有扇入模式,就有扇出模式,扇出模式是和扇入模式相反的。
扇出模式只有一个输入源 Channel,有多个目标 Channel,扇出比就是 1 比目标 Channel 数的值,经常用在设计模式中的观察者模式中(观察者设计模式定义了对象间的一种一对多的组合关系。这样一来,一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动刷新)。在观察者模式中,数据变动后,多个观察者都会收到这个变更信号。
下面是一个扇出模式的实现。从源 Channel 取出一个数据后,依次发送给目标 Channel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func fanOut(ch <-chan interface{}, out ...chan interface{}) {
go func() {
defer func() { // 退出时关闭所有的输出chan
for i := 0; i < len(out); i++ {
close(out[i])
}
}()
for v := range ch { // 从输入chan中读取数据
v := v
for i := 0; i < len(out); i++ {
i := i
out[i] <- v // 放入到输出chan中,同步方式
}
}
}()
}
|
Stream
这里我来介绍一种把 Channel 当作流式管道使用的方式,也就是把 Channel 看作流(Stream),提供跳过几个元素,或者是只取其中的几个元素等方法。
首先,我们提供创建流的方法。这个方法把一个数据 slice 转换成流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func asStream(done <-chan struct{}, values ...interface{}) <-chan interface{} {
s := make(chan interface{}) //创建一个unbuffered的channel
go func() { // 启动一个goroutine,往s中塞数据
defer close(s) // 退出时关闭chan
for _, v := range values { // 遍历数组
select {
case <-done:
return
case s <- v: // 将数组元素塞入到chan中
}
}
}()
return s
}
|
流创建好以后,该咋处理呢?下面我再给你介绍下实现流的方法。
- takeN:只取流中的前 n 个数据;
- takeFn:筛选流中的数据,只保留满足条件的数据;
- takeWhile:只取前面满足条件的数据,一旦不满足条件,就不再取;
- skipN:跳过流中前几个数据;
- skipFn:跳过满足条件的数据;
- skipWhile:跳过前面满足条件的数据,一旦不满足条件,当前这个元素和以后的元素都会输出给 Channel 的 receiver。
这些方法的实现很类似,我们以 takeN 为例来具体解释一下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func takeN(done <-chan struct{}, valueStream <-chan interface{}, num int) <-chan interface{} {
takeStream := make(chan interface{}) // 创建输出流
go func() {
defer close(takeStream)
for i := 0; i < num; i++ { // 只读取前num个元素
select {
case <-done:
return
case takeStream <- <-valueStream: //从输入流中读取元素
}
}
}()
return takeStream
}
|
Map-Reduce
map-reduce 是一种处理数据的方式,最早是由 Google 公司研究提出的一种面向大规模数据处理的并行计算模型和方法,开源的版本是 hadoop,前几年比较火。
不过,我要讲的并不是分布式的 map-reduce,而是单机单进程的 map-reduce 方法。
map-reduce 分为两个步骤,第一步是映射(map),处理队列中的数据,第二步是规约(reduce),把列表中的每一个元素按照一定的处理方式处理成结果,放入到结果队列中。
就像做汉堡一样,map 就是单独处理每一种食材,reduce 就是从每一份食材中取一部分,做成一个汉堡。
我们先来看下 map 函数的处理逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func mapChan(in <-chan interface{}, fn func(interface{}) interface{}) <-chan interface{} {
out := make(chan interface{}) //创建一个输出chan
if in == nil { // 异常检查
close(out)
return out
}
go func() { // 启动一个goroutine,实现map的主要逻辑
defer close(out)
for v := range in { // 从输入chan读取数据,执行业务操作,也就是map操作
out <- fn(v)
}
}()
return out
}
|
reduce 函数的处理逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
func reduce(in <-chan interface{}, fn func(r, v interface{}) interface{}) interface{} {
if in == nil { // 异常检查
return nil
}
out := <-in // 先读取第一个元素
for v := range in { // 实现reduce的主要逻辑
out = fn(out, v)
}
return out
}
|
我们可以写一个程序,这个程序使用 map-reduce 模式处理一组整数,map 函数就是为每个整数乘以 10,reduce 函数就是把 map 处理的结果累加起来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// 生成一个数据流
func asStream(done <-chan struct{}) <-chan interface{} {
s := make(chan interface{})
values := []int{1, 2, 3, 4, 5}
go func() {
defer close(s)
for _, v := range values { // 从数组生成
select {
case <-done:
return
case s <- v:
}
}
}()
return s
}
func main() {
in := asStream(nil)
// map操作: 乘以10
mapFn := func(v interface{}) interface{} {
return v.(int) * 10
}
// reduce操作: 对map的结果进行累加
reduceFn := func(r, v interface{}) interface{} {
return r.(int) + v.(int)
}
sum := reduce(mapChan(in, mapFn), reduceFn) //返回累加结果
fmt.Println(sum)
}
|
参考
ratel