go并发控制

go实现对并发控制的几种方式

  • WaitGroup
  • Channel + Select
  • Context

WaitGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
time.Sleep(3 * time.Second)
fmt.Println("<groutine 1> done")
wg.Done()
}()
go func() {
fmt.Println("<groutine 2> done")
wg.Done()
}()
wg.Wait()
fmt.Println("--- All groutine finnish ---")
}

输出

1
2
3
<groutine 2> done
<groutine 1> done
--- All groutine finnish ---

Channel + Select

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
// 收到stop channel,结束goroutine
return
default:
// 不做处理,等待stop channel
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
// 停止goroutine
stop <- true
time.Sleep(5 * time.Second)
}

Context

概述

Go 语言中的每一个请求的都是通过一个单独的 Goroutine 进行处理的,HTTP/RPC 请求的处理器往往都会启动新的 Goroutine 访问数据库和 RPC 服务,我们可能会创建多个 Goroutine 来处理一次请求,而 Context 的主要作用就是在不同的 Goroutine 之间同步请求特定的数据、取消信号以及处理请求的截止日期

Context的调用是链式的,通过WithCancelWithDeadlineWithTimeoutWithValue派生出新的 Context。当父Context被取消时,其派生的所有Context都将取消

使用

启动了3个goroutine进行不断的循环等待,每一个都使用了Context进行跟踪,当使用cancel函数通知取消时,所有基于这个Context或者衍生的子Context都会收到通知,当Context取消时,得到一个关闭channel(ctx.Done),从关闭的ctx.Done可以读取值,退出select,最终释放goroutine。

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
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "【监控1】")
go watch(ctx, "【监控2】")
go watch(ctx, "【监控3】")

time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
fmt.Println("main退出")

}

func watch(ctx context.Context, name string) {
for {
select {
case d, _ := <-ctx.Done():
fmt.Printf("%s ctx.Done:<%v> ctx.Err:<%v> 监控退出,停止了...\n", name, d, ctx.Err())
return
default:
fmt.Println(name, "goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}

输出

1
2
3
4
5
6
7
8
【监控3】 goroutine监控中...
【监控1】 goroutine监控中...
【监控2】 goroutine监控中...
可以了,通知监控停止
【监控3】 ctx.Done:<{}> ctx.Err:<context canceled> 监控退出,停止了...
【监控1】 ctx.Done:<{}> ctx.Err:<context canceled> 监控退出,停止了...
【监控2】 ctx.Done:<{}> ctx.Err:<context canceled> 监控退出,停止了...
main退出

实现

核心Context接口

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • Deadline:返回当前 Context 被取消的时间,也就是完成工作的截止日期
  • Done:返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消之后关闭,多次调用 Done 方法会返回同一个 Channel
  • Err:返回当前 Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值
    • 如果当前 Context 被取消就会返回 Canceled 错误;
    • 如果当前 Context 超时就会返回 DeadlineExceeded 错误
  • Value:从 Context 中返回键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,这个功能可以用来传递请求特定的数据

根Context

golang默认的context包已经有emptyCtx实现了Contex接口,具体为BackgroundTODO两个context。其中,context.Background() 是context上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background() 演化出来。(TODO实际基本很少用,只有当不知道该使用什么Context的时候,可以使用这个)

1
2
3
4
5
6
7
8
9
10
11
12
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

func Background() Context {
return background
}

func TODO() Context {
return todo
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}

func (*emptyCtx) Done() <-chan struct{} {
return nil
}

func (*emptyCtx) Err() error {
return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}

Context的继承衍生

通过以下With函数,可创建一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

1
2
3
4
5
6
7
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithValue(parent Context, key, val interface{}) Context
  • WithCancel:传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。

  • WithDeadline:和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。

  • WithTimeout:和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。

  • WithValue:此函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到,这是我们实际用经常要用到的技巧,一般我们想要通过上下文来传递数据时,可以通过这个方法,如我们需要tarce追踪系统调用栈的时候

Reference


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!