Go语言并发控制总结

Go语言的并发控制

Go语言原生支持的三种常用并发控制有ChannelWaitGroupContext等;

  • ① Channel:使用chan控制子协程;
  • ② WaitGroup:使用信号量机制控制子协程;
  • ③ Context:使用上下文控制子协程;

三种方式各有优缺点:

  • Channel的优点是:实现简单,流程清晰易懂;
  • WaitGroup的优点是:子协程数可动态调整;
  • Context的优点是:对子协程派生出来的孙子协程的控制;

Channel使用chan控制子协程

chan是Go语言原生支持的通道类型、用于协程之间的通信;也可以用于并发控制;

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
package main

import (
"fmt"
"time"
)

// channel 并发控制
func main() {
channels := make([]chan int, 10) // 创建一个包含10个元素的切片,元素类型为channel

for i := 0; i < 10; i++ {
// 在切片中放入一个channel
channels[i] = make(chan int)
// 启动协程,传入一个管道用于通信
go Process(channels[i])
}

// 遍历切片,等待子协程结束
for i, ch := range channels {
<-ch
fmt.Println("routine ", i, " quit!")
}
}

func Process(ch chan int) {
// do some work...
time.Sleep(time.Second * 1)

// 在管道中写入一个元素、表示当前协程已结束
ch <- 1
}

Go语言生产者和消费者的例子

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
package main

import "fmt"

// 消费者
func consumer(data chan int, done chan bool) {
// 接收数据,直到通道被关闭
for v := range data {
fmt.Println("recv:", v)
}
// 通知main消费结束
done <- true
}

// 生产者
func producer(data chan int) {
for i := 0; i < 4; i++ {
// 发送数据
data <- i
}
// 生产结束,关闭通道
close(data)
}

func main() {
done := make(chan bool) // 用于接收消费者结束信号
data := make(chan int) // 数据管道
go consumer(data, done) // 启动消费者
go producer(data) // 启动生产者
<-done // 阻塞,直到消费者发回结束信号
}

使用Channel并发控制的总结:

使用Channel控制子协程的优点是:实现简单;

缺点是:当需要大量创建子协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制;

WaitGroup使用信号量机制(等待组)控制子协程

WaitGroup是Go应用开发中经常用的并发控制技术;WaitGroup对外提供了三个接口:

  • ① Add(delta int):将delta值加到counter中;
  • ② Wait:阻塞等待信号量;
  • ③ Done:counter递减1,按照waiter数值释放响应次数的信号量;
注意: Add操作必须早于Wait(),否则会触发panic;

Add设置的值必须与实际等待的goroutine的个数一致,否则会触发panic;

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
package main

import (
"fmt"
"sync"
"time"
)

func main() {
var wg sync.WaitGroup

wg.Add(2)

go func() {
// do some work
time.Sleep(time.Second)

fmt.Println("finished!")

wg.Done()
}()

go func() {

// do some work
time.Sleep(time.Second)

wg.Done()
}()

wg.Wait()
fmt.Println("ok")
}

Context使用上下文控制子协程

Go语言的Context是开发中经常用到的并发控制技术;它与WaitGroup最大的不同点是:

context对于派生goroutine有更强的控制力,它可以控制多级的goroutine;context翻译成中文是”上下文”;

源码:src/context/context.go:Context定义了该接口:

1
2
3
4
5
6
7
8
9
type Context interface {
Deadline() (deadline time.Time, ok bool)

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}
}

一个典型的cancel context使用例子:

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
package main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
go HandelRequest(ctx)

time.Sleep(time.Second * 5)
fmt.Println("its time to stop all sub goroutine!")
cancel()

time.Sleep(time.Second * 5)
}

func HandelRequest(ctx context.Context) {
go WriteRedis(ctx)
go WriteDatabase(ctx)
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done.")
return
default:
fmt.Println("HandelRequest running.")
time.Sleep(time.Second * 2)
}
}
}

func WriteRedis(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteRedis Done.")
return
default:
fmt.Println("WriteRedis running.")
time.Sleep(time.Second * 2)
}
}
}

func WriteDatabase(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("WriteDatabase Done.")
return
default:
fmt.Println("WriteDatabase running.")
time.Sleep(time.Second * 2)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
WriteDatabase running.
WriteRedis running.
HandelRequest running.
HandelRequest running.
WriteRedis running.
WriteDatabase running.
WriteDatabase running.
WriteRedis running.
HandelRequest running.
its time to stop all sub goroutine!
HandelRequest Done.
WriteRedis Done.
WriteDatabase Done.