0%

通知子 goroutine 退出的三种方式

通知子 goroutine 退出的三种方式

1 全局变量

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

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

var wg sync.WaitGroup
var exit bool

func worker() {
defer wg.Done()
for {
fmt.Println("worker")
time.Sleep(time.Second)
if exit {
break
}
}
}

func main() {
wg.Add(1)
go worker()
time.Sleep(time.Second * 5)
exit = true
wg.Wait()
fmt.Println("over")
}
  • 定义全局变量 exit 用于表示是否要求子 goroutine 退出
  • 父 goroutine 中设置全局变量 exit 的值
  • 子 goroutine 中循环检测全局变量 exit 的值,如果为 true,则结束这个子 goroutine

2 Channel

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

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

var wg sync.WaitGroup

func worker(exitChan chan struct{}) {
defer wg.Done()
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-exitChan:
break LOOP
default: // 注意:如果没有default,select会阻塞在等待exitChan有值
}
}
}

func main() {
var exitChan = make(chan struct{})
wg.Add(1)
go worker(exitChan)
time.Sleep(time.Second * 3)
exitChan <- struct{}{}
close(exitChan)
wg.Wait()
fmt.Println("over")
}
  • 定义无缓冲的管道 exitChan 用于表示是否要求子 goroutine 退出
  • select 多路复用,监听管道 exitChan 中是否有值。如果有值就结束子 goroutine;如果没有值,就执行默认操作
  • 注意:如果没有default,select会阻塞在等待exitChan有值,因为这里定义的管道是无缓冲的

3 Context

3.1 context.WithCancel

3.1.1 开启1个子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
28
29
30
31
32
33
34
package main

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
defer wg.Done()
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default: // 注意:如果没有default,select会阻塞在等待ctx.Done()有值
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子 goroutine 结束
wg.Wait()
fmt.Println("over")
}
  • context.WithCancel 函数返回父context的子context,和一个函数对象cancel用于通知子goroutine结束
  • context内部维护了一个管道,context对象ctx调用Done方法,返回只读形式的管道
  • 子goroutine中select多路复用,监听管道ctx.Done。如果有值,则结束子goroutine;否则,执行默认操作
  • 调用 cancel 函数对象,通知子goroutine结束

3.1.2 开启多个子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
28
29
30
31
32
33
34
35
36
37
38
39
package main

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

var wg sync.WaitGroup

func workerGroup(ctx context.Context) {
go worker(ctx)
go worker(ctx)
}

func worker(ctx context.Context) {
defer wg.Done()
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default: // 注意:如果没有default,select会阻塞在等待ctx.Done()有值
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(2)
go workerGroup(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子 goroutine 结束
wg.Wait()
fmt.Println("over")
}
  • 使用context可以很方便地处理通知多个子goroutine结束的情况

3.2 context.WithDeadline

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

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
defer wg.Done()
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Millisecond * 10)
select {
case <-ctx.Done():
break LOOP
default: // 注意:如果没有default,select会阻塞在等待exitChan有值
}
}
}

func main() {
deadline := time.Now().Add(time.Millisecond * 50)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子 goroutine 结束
wg.Wait()
fmt.Println("over")
}
  • 上面的代码,定义了一个50ms过期的deadline
  • 调用context.WithDeadline函数,得到子上下文ctx,和一个取消函数cancel
  • 子goroutine中select多路复用,监听管道ctx.Done。如果有值,则结束子goroutine;否则,执行默认操作
  • 两种情况会向管道 ctx.Done 中发送结束信号:① 截止时间到,ctx对象自动向内部维护的管道发送结束信号 ② 手动调用 cancel 函数,通知子 goroutine 结束

3.3 context.WithTimeout

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

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

var wg sync.WaitGroup

func worker(ctx context.Context) {
defer wg.Done()
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Millisecond * 10)
select {
case <-ctx.Done():
break LOOP
default: // 注意:如果没有default,select会阻塞在等待exitChan有值
}
}
}

func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 3)
cancel() // 通知子 goroutine 结束
wg.Wait()
fmt.Println("over")
}
  • 调用context.WithTimeout函数,指定超时时间,得到子上下文ctx,和一个取消函数cancel
  • 子goroutine中select多路复用,监听管道ctx.Done。如果有值,则结束子goroutine;否则,执行默认操作
  • 两种情况会向管道 ctx.Done 中发送结束信号:① 超时时间到,ctx对象自动向内部维护的管道发送结束信号 ② 手动调用 cancel 函数,通知子 goroutine 结束