前面说到了并发的子协程groutine启动以后,它与主协程是完全独立运行。有时候为了避免出现子协程的任务还没处理完,主协程已经结束的尴尬情况。我们需要使用WaitGroup,确保主协程要等待所有的子协程都运行完毕以后再彻底退出结束程序。如果出现一种场景,我们想根据自己的意愿停止子协程的运行,这就需要使用context功能来实现了。

假设有100名玩家参加了一场抽奖活动,每个参与者有无数次机会从0-99的整数中抽取数字幸运数字<5>,每次抽取间隔1s。主办方规定本次比赛,产生10名幸运者以后就结束。

context.WithCancel()

因为子groutine是死循环方式运行,不会自动终结。所以不需要WaitGroup 等待子协程结束

package main

import (
"context"
"fmt"
"math/rand"
"time"
)

// ctx上下文必须作为第一个参数传递
func LuckMan(ctx context.Context, groutine_id int, ch chan<- string) {
// 给随机数产生的时候加盐
rand.Seed(time.Now().UnixNano())
for {
select {
// 检测到关闭信号就退出子协程
case <-ctx.Done():
return

default:
// 从0-99的随机数中与幸运数字比对
if rand.Intn(100) == 5 {
ret := fmt.Sprintf("第 <%d> 号groutine 拿到了幸运数字5", groutine_id)
// 将中奖结果写入通道
ch <- ret
}
time.Sleep(time.Second * 1)
}
}
}

func main() {
// 创建一个用于存储中奖结果的channel
ch := make(chan string, 10)

// 创建一个输出 WithCancel()信号的context实例
ctx, cancel := context.WithCancel(context.Background())

// 100个玩家并发参与抽奖
for i := 1; i < 101; i++ {
go LuckMan(ctx, i, ch)
}

// 循环检测中奖人数
for {
switch len(ch) {

case 10:
// 因为子groutine负责写操作,为了避免重复关闭操作,需要在主协程闭关通道
// 通道不关闭,子groutine会被hold住无法关闭
close(ch)
// 发送ctx.Done 信号关闭所有子groutine
cancel()

fmt.Printf("---- 比赛结束 ---- \n")

// 取出中奖人员名单
for luckman := range ch {
fmt.Printf("<%d>: %s\n", len(ch), luckman)
}

return

}
}
}

运行结果

%  go run main.go
---- 比赛结束 ----
<9>: 第 <21> 号groutine 拿到了幸运数字5
<8>: 第 <91> 号groutine 拿到了幸运数字5
<7>: 第 <25> 号groutine 拿到了幸运数字5
<6>: 第 <14> 号groutine 拿到了幸运数字5
<5>: 第 <35> 号groutine 拿到了幸运数字5
<4>: 第 <79> 号groutine 拿到了幸运数字5
<3>: 第 <50> 号groutine 拿到了幸运数字5
<2>: 第 <36> 号groutine 拿到了幸运数字5
<1>: 第 <4> 号groutine 拿到了幸运数字5
<0>: 第 <82> 号groutine 拿到了幸运数字5


第二轮主办方修改了抽奖规则。抽奖的条件不变,还是每1s抽一次。新规则是500名玩家一起参与抽奖,限时10s,中奖人数无上限。

context.WithTimeout()

因为本次比赛时间由timeout,因为可以启用WaitGroup 等待子协程结束

package main

import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)

var wg sync.WaitGroup

// 抽奖玩家的函数不用修改
func LuckMan(ctx context.Context, groutine_id int, ch chan<- string) {
defer wg.Done()
// 给随机数产生的时候加盐
rand.Seed(time.Now().UnixNano())
for {
select {
// 检测到关闭信号就退出子协程
case <-ctx.Done():
return

default:
// 从0-99的随机数中与幸运数字比对
if rand.Intn(100) == 5 {
ret := fmt.Sprintf("第 <%d> 号groutine 拿到了幸运数字5 ", groutine_id)
// 将中奖结果写入通道
ch <- ret
}
time.Sleep(time.Second * 1)
}
}
}

func main() {
// 创建一个用于存储中奖结果的channel
ch := make(chan string, 1000)

// 创建一个输出 WithTimeout()需要入超时时间
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)

wg.Add(500)
// 500个玩家并发参与抽奖
for i := 1; i < 501; i++ {
go LuckMan(ctx, i, ch)
}

// 这次主办方能掌控结束时间,就可以安排等待了
wg.Wait()
close(ch)
// 正常情况到了超时时间,ctx就会发送终止信号
// 这里是以防万一,再次手动执行一次终止。
cancel()

fmt.Printf("---- 比赛结束 ---- \n")

// 取出中奖人员名单
if len(ch) > 0 {
for luckman := range ch {
fmt.Printf("<%d>: %s\n", len(ch), luckman)
}
} else {
fmt.Println("很遗憾没有玩家中奖")
}

}

连续两次运行结果,每次中奖人数都不同

%  go run main.go
---- 比赛结束 ----
<17>: 第 <81> 号groutine 拿到了幸运数字5
<16>: 第 <108> 号groutine 拿到了幸运数字5
<15>: 第 <169> 号groutine 拿到了幸运数字5
<14>: 第 <241> 号groutine 拿到了幸运数字5
<13>: 第 <2> 号groutine 拿到了幸运数字5
<12>: 第 <20> 号groutine 拿到了幸运数字5
<11>: 第 <42> 号groutine 拿到了幸运数字5
<10>: 第 <43> 号groutine 拿到了幸运数字5
<9>: 第 <131> 号groutine 拿到了幸运数字5
<8>: 第 <205> 号groutine 拿到了幸运数字5
<7>: 第 <238> 号groutine 拿到了幸运数字5
<6>: 第 <247> 号groutine 拿到了幸运数字5
<5>: 第 <316> 号groutine 拿到了幸运数字5
<4>: 第 <322> 号groutine 拿到了幸运数字5
<3>: 第 <406> 号groutine 拿到了幸运数字5
<2>: 第 <98> 号groutine 拿到了幸运数字5
<1>: 第 <188> 号groutine 拿到了幸运数字5
<0>: 第 <280> 号groutine 拿到了幸运数字5

% go run main.go
---- 比赛结束 ----
<12>: 第 <327> 号groutine 拿到了幸运数字5
<11>: 第 <446> 号groutine 拿到了幸运数字5
<10>: 第 <440> 号groutine 拿到了幸运数字5
<9>: 第 <151> 号groutine 拿到了幸运数字5
<8>: 第 <434> 号groutine 拿到了幸运数字5
<7>: 第 <37> 号groutine 拿到了幸运数字5
<6>: 第 <181> 号groutine 拿到了幸运数字5
<5>: 第 <245> 号groutine 拿到了幸运数字5
<4>: 第 <269> 号groutine 拿到了幸运数字5
<3>: 第 <354> 号groutine 拿到了幸运数字5
<2>: 第 <352> 号groutine 拿到了幸运数字5
<1>: 第 <440> 号groutine 拿到了幸运数字5
<0>: 第 <454> 号groutine 拿到了幸运数字5


第三轮主办方进一步完善规则。因为连续2次都使用<5>作为幸运数字,给一些人留下了作弊的空间。所以这一轮将随机从<0-99>之间选取一个整数作为幸运数字。其他规则与第二轮不变。

context.WithValue()

通过context为子groutine传递KV格式的键值对

package main

import (
"context"
"fmt"
"math/rand"
"sync"
"time"
)

var wg sync.WaitGroup

// 抽奖玩家的函数不用修改
func LuckMan(ctx context.Context, groutine_id int, ch chan<- string) {
defer wg.Done()
// 给随机数产生的时候加盐
rand.Seed(time.Now().UnixNano())
for {
select {
// 检测到关闭信号就退出子协程
case <-ctx.Done():
return

default:
// 从0-99的随机数中与幸运数字比对
if rand.Intn(100) == ctx.Value("lucky_num") {
ret := fmt.Sprintf("第 <%d> 号groutine 拿到了幸运数字 %d ", groutine_id, ctx.Value("lucky_num"))
// 将中奖结果写入通道
ch <- ret
}
time.Sleep(time.Second * 1)
}
}
}

func main() {
// 创建一个用于存储中奖结果的channel
ch := make(chan string, 1000)

// 创建一个输出 WithTimeout()需要入超时时间
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)

rand.Seed(time.Now().UnixNano())
Luck_n := rand.Intn(100)
fmt.Println("本次抽奖的幸运数字是: ", Luck_n)
// 通过WithValue()方法,把幸运数字绑定到Timeout这个ctx中,一并传给子协程
// 还是赋值给已经声明好的ctx变量
ctx = context.WithValue(ctx, "lucky_num", Luck_n)

wg.Add(500)
// 500个玩家并发参与抽奖
for i := 0; i < 500; i++ {
// 函数的接收参数完全不用修改
go LuckMan(ctx, i, ch)
}

// 这次主办方能掌控结束时间,就可以安排等待了
wg.Wait()
close(ch)
// 正常情况到了超时时间,ctx就会发送终止信号
// 这里是以防万一,再次手动执行一次终止。
cancel()

fmt.Printf("---- 比赛结束 ---- \n")

// 取出中奖人员名单
if len(ch) > 0 {
for luckman := range ch {
fmt.Printf("<%d>: %s\n", len(ch), luckman)
}
} else {
fmt.Println("很遗憾没有玩家中奖")
}
}

运行结果

%  go run main.go
本次抽奖的幸运数字是: 58
---- 比赛结束 ----
<66>: 第 <30> 号groutine 拿到了幸运数字 58
<65>: 第 <141> 号groutine 拿到了幸运数字 58
<64>: 第 <414> 号groutine 拿到了幸运数字 58
<63>: 第 <415> 号groutine 拿到了幸运数字 58
<62>: 第 <36> 号groutine 拿到了幸运数字 58
<61>: 第 <65> 号groutine 拿到了幸运数字 58
<60>: 第 <151> 号groutine 拿到了幸运数字 58
...

% go run main.go
本次抽奖的幸运数字是: 88
---- 比赛结束 ----
<49>: 第 <97> 号groutine 拿到了幸运数字 88
<48>: 第 <20> 号groutine 拿到了幸运数字 88
<47>: 第 <179> 号groutine 拿到了幸运数字 88
<46>: 第 <304> 号groutine 拿到了幸运数字 88
<45>: 第 <394> 号groutine 拿到了幸运数字 88
<44>: 第 <481> 号groutine 拿到了幸运数字 88
<43>: 第 <452> 号groutine 拿到了幸运数字 88
...