golang并发之秦皇寻找长生药 2 years ago
博客大纲
晴天霹雳
经过6代秦王的努力,到秦始皇即位的时候,秦国国力达到空前,13岁的他为了完成父辈、祖辈们的遗愿,励精图治,奋发图强,终于在39岁的时候一扫六合,统一天下。
此时的秦皇功勋卓著,威震海内,可谓风光无限,然而他却感觉到身体的不适. 查阅古籍《易经》占卜得知,自己最多只能活10年.
秦皇顿时慌了,他还有很多大事没做,为了给身体续命,秦皇把各大部门CEO招回咸阳,开了个会议:
会议的主题是: 搜索全国各大药店,不计一切代价找到下图所示的长生药.
与会主要CEO名单:
- 蒙恬
- 李斯
- 赵高
- 扶苏
- 胡亥
- 吕不韦
- 徐福
- 章邯
- 王翦
秦皇寻找长生药1.0 - 要求所有人完成任务
接到命令的众人,带着人马陆续出发了.
9大人马,向全国各地奔赴,长城,沙漠,蓬莱…
丸子也接到了秦皇的召见,当场给秦皇做了2种方案
1.WaitGroup
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
for i := 0; i < len(queens); i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
doTask(queens[n])
}(i)
}
wg.Wait()
}
func doTask(name string) {
fmt.Printf("[%s]:出发了\n", name)
rand.Seed(time.Now().Unix())
for {
//模拟找长生药的过程
time.Sleep(time.Millisecond * 200)
n := rand.Intn(10)
if n == 8 {
fmt.Printf("[%s]:完成了任务\n", name)
break
}
}
}
模拟运行2次:
2.channel
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
allCompleted()
}
func allCompleted() {
ch := make(chan string)
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
for i := 0; i < len(queens); i++ {
go func(n int) {
res := doTask(queens[n])
ch <- res
}(i)
}
for i := 0; i < len(queens); i++ {
fmt.Println(<-ch)
}
}
func doTask(name string) string {
fmt.Printf("[%s]:出发了\n", name)
rand.Seed(time.Now().Unix())
for {
//模拟找长生药的过程
time.Sleep(time.Millisecond * 200)
n := rand.Intn(10)
if n == 8 {
return fmt.Sprintf("[%s]:完成了任务", name)
}
}
}
模拟运行2次:
秦皇寻找长生药2.0 - 任意人完成任务即可
经过了一段时间的搜寻,秦皇发现,要求所有人都完成任务不仅消耗财力,而且效率低下,事实是 - 只要有一对人马完成任务即可,因此寻找长生药升级成了2.0
buffered channel
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
result := onlyOneCompleted()
fmt.Println(result)
}
func onlyOneCompleted() string {
ch := make(chan string)
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
for i := 0; i < len(queens); i++ {
go func(n int) {
res := doTask(queens[n])
ch <- res
}(i)
}
return <-ch
}
func doTask(name string) string {
fmt.Printf("[%s]:出发了\n", name)
rand.Seed(time.Now().Unix())
for {
//模拟找长生药的过程
time.Sleep(time.Millisecond * 200)
n := rand.Intn(10)
if n == 8 {
return fmt.Sprintf("[%s]:完成了任务", name)
}
}
}
模拟运行2次:
看着结果 好像是没问题,其实上面的代码存在安全隐患,我们知道:channel的2端不论是发送方还是接收方只要一方不在线 另一方就会阻塞
func main() {
fmt.Println("before:", runtime.NumGoroutine())
result := onlyOneCompleted()
fmt.Println(result)
time.Sleep(time.Second * 10)
fmt.Println("after:", runtime.NumGoroutine())
}
模拟运行2次:
可以看到除了main goroutine外,还有8个被阻塞的goroutine,这种“代码缺陷”在当前示例中是没有任何影响的
但如果是服务器中或者后台程序,那么goroutine成倍的增长势必会在某一时刻将服务器的内存撑爆,而导致内存泄漏(memory leak)
所以上述代码还需要改进,改进也很简单,就是不让goroutine阻塞即可,buffered channel就可以做到.
func onlyOneCompleted() string {
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
ch := make(chan string, len(queens))
for i := 0; i < len(queens); i++ {
go func(n int) {
res := doTask(queens[n])
ch <- res
}(i)
}
return <-ch
}
改完代码之后,我们再来测试一下:
秦皇寻找长生药3.0 - 任意人完成任务即可
2.0虽然可以完成任务,但是资源却存在极大的浪费,其他8个人可没闲着,一直在“内耗”呢!
所以需要通知其他所有人 - “事已经办妥了,大家都休息吧”
close(channel)
就可以达到广播的效果
我们来改造一下代码
func onlyOneCompleted() string {
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
ch := make(chan string)
for i := 0; i < len(queens); i++ {
go func(n int) {
res := doTask(queens[n])
ch <- res
}(i)
}
result := <-ch
close(ch)
return result
}
运行后,发现报错了,原因是向已经关闭的channel中发送数据
切记: channel的close是在发送方
既然这种方案行不通, 我们来试试context
context
context比较强大,
在Golang中,Context是用于控制协程退出的工具。Context可以让我们在多个goroutine之间传递取消信号,并且能够及时终止协程的执行。它非常适用于一些需要及时停止的场景,比如网络请求、任务执行等。
使用Context的基本流程如下:
- 1.使用context包创建一个Context对象。
- 2.在需要控制的goroutine中,调用Context的Done方法来检查是否需要退出。
- 3.如果需要退出,则退出当前goroutine。
package main
import (
"context"
"fmt"
"math/rand"
"runtime"
"time"
)
func main() {
fmt.Println("before:", runtime.NumGoroutine())
onlyOneCompleted()
// time.Sleep(time.Second * 10)
for {
time.Sleep(time.Second * 1)
fmt.Println("after:", runtime.NumGoroutine())
}
}
func onlyOneCompleted() {
done := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
var queens = []string{
"蒙恬",
"李斯",
"赵高",
"扶苏",
"胡亥",
"吕不韦",
"徐福",
"章邯",
"王翦",
}
for i := 0; i < len(queens); i++ {
go func(n int, ctx context.Context) {
fmt.Printf("[%s]:出发了\n", queens[n])
for {
rand.Seed(time.Now().Unix())
//模拟找长生药的过程
time.Sleep(time.Millisecond * 20)
x := rand.Intn(3)
if x == 1 {
//必须在最后一步,即即将判定任务完成那里去check一下是否有 <-ctx.Done()的信号
//否则总是会有那么几个携程被done阻塞 一直无法退出 导致内存泄漏
if isCancelled(ctx) {
fmt.Printf("%s was cancelled\n", queens[n])
return
}
done <- fmt.Sprintf("[%s]:完成了任务", queens[n])
// fmt.Printf("[%s]:完成了任务\n", queens[n])
// cancel()
return
}
}
}(i, ctx)
}
fmt.Println(<-done)
cancel()
}
func isCancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
查看输出 是我们想要的结果
- 上一篇: ios flutter开发环境搭建
- 下一篇: 大厂Golang开发工程师面试题集锦