GO的并发操作
为什么时隔已久我会再写文章呢?原来是佐天,有个学长问我,他说“小杨,我出个题考考你”。我说莫得问题,我来做。学长立马把题发上来了,啪的一下!很快啊!
题目是这样的:
现在有很多人(一个随机数),从中挑出来10个人让他们进行m轮游戏。是什么游戏呢?游戏是这样的,他们十个人在每轮游戏中都会报一个数,这个数是什么是随机的(不必纠结太多,用rand.Intn()就行),他们将这个数报告给裁判。裁判在每轮也会选一个数,如果某个人报的数字和裁判一样的话,那这个人就out了。现在要求你写一串小代码,要求不能使用全局变量,问m轮后还剩几人?
我一看题,嚯!这肯定要用我最近才学的go的并发来操作。只要创造十个协程,把东西发给裁判,如果不行就让这个人滚蛋。于是我写出了下面这段代码
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func main() {
var m int
fmt.Scanf("%d", &m)
var sum int = 10
for i := 0; i < m; i++ {
ch1 := make(chan int, 10)
for i := 1; i <= sum; i++ {
go func() {
num := rand.Intn(10)
ch1 <- num
runtime.Gosched()
}()
}
time.Sleep(time.Second * 1)
close(ch1)
k := rand.Intn(10)
for num := range ch1 {
if num == k {
sum--
break
}
}
fmt.Printf("%d", sum)
}
}
我自信慢慢的把代码发给学长,脸上带着喜悦的笑容。欧耶!
可令我没想到的是,我的代码在逻辑上有些问题,因为我在暂停主协程的时候用的是time.sleep(),所以如果在我子协程完成前我就关闭channel了,那指定不行,选手不就无辜退赛了么!所以我就想到了使用sync包里面的waitgroup,来确保我协程运行的顺序没有问题。
以下代码是我的优化方案
package main
import (
"fmt"
"math/rand"
"sync"
)
func main() {
var m int
fmt.Scanf("%d", &m)
var wg sync.WaitGroup
var sum int = 10
for i := 0; i < m; i++ {
ch1 := make(chan int, 10)
//wg.Add(10)
for i := 1; i <= sum; i++ {
wg.Add(1)
go func() {
defer wg.Done()
num := rand.Intn(10)
ch1 <- num
}()
}
wg.Wait()
//time.Sleep(time.Second * 1)
close(ch1)
k := rand.Intn(10)
for num := range ch1 {
if num == k {
sum--
break
}
}
fmt.Printf("%d\n", sum)
}
}
这次总算是对了吧!我心里想着,把代码交给学长。
学长看到后,呃呃呃,你没有真正理解题的意思,可按照你这么想,你这个代码也写得过于复杂了,还是让我来吧!
果然大佬出手就知道有没有!以下是学长的代码
// 这个的物理意义是
// 进行M轮游戏
// 从无数个人中选n个人让他们随便报一个数
// 然后裁判判断是不是踩雷了
// 统计一下没有踩到雷的数量人
// 然后下一轮再找没有踩到雷数量的人进行一轮游戏
// 它并不是原先的n个人 因为他们的goroutine的地址都不一样
func game2() {
ch := make(chan int)
cnt := N
for i := 0; i < M; i++ {
myCnt := cnt
for j := 0; j < myCnt; j++ {
go func() {
ch <- rand.Intn(MaxNumber)
}()
}
num := rand.Intn(MaxNumber)
for j := 0; j < myCnt; j++ {
if val := <- ch; val == num {
cnt--
}
}
}
fmt.Println(M, " 轮游戏还有",cnt,"个人")
}
看看,看看! 看看人家写的代码,这个是按照我们思路写的,写的比我们嗯嗯嗯嗯。。。。我就不说什么了,我有点害羞。(脸红脸红脸红)
可接下来看到的,才是这个题原本的样貌。以及我从这个题中所获得的许多新知识。
package main
import (
"fmt"
"math/rand"
"sync"
)
func game1() {
const (
N = 10 //10个人
M = 10 // 10轮游戏
MaxNumber = 10
gameOver = 0
gameNext = 1
)
ch := make(chan chan int) //这里定义了一个管道类型的管道
readyGame := sync.WaitGroup{}
readyGame.Add(N)
start := sync.WaitGroup{} //start是用来控制每轮游戏暂停,保证没有人先开下一轮的
// N个人
for i := 0; i < N; i++ {
go func() {
//每个人执行M轮游戏
phone := make(chan int)
for j := 0; j < M; j++ {
// 告诉裁判我可以开始游戏了
readyGame.Done()
ch <- phone //注意这里是将phone给了ch,不是从phone里取值
phone <- rand.Intn(MaxNumber)
//裁判是通过ch里面的phone里面的val来判断你有没有出局
if val := <-phone; val == gameOver {
break
}
//这一轮游戏结束等待
// 如果没有这一步的话 他会直接进入到写管道 这个时候他可能提前进入下一轮游戏 这样是不对的
start.Wait()
}
}()
}
//裁判
start.Add(1)
cnt := N
for i := 0; i < M; i++ {
num := rand.Intn(MaxNumber) //裁判选数字
tempCnt := cnt
for j := 0; j < tempCnt; j++ {
phone := <-ch
val := <-phone
//fmt.Println(val)
if num == val {
phone <- gameOver
cnt--
} else {
phone <- gameNext
}
}
if i == M-1 {
break
}
readyGame.Add(cnt) //给剩下的人发通行证
start.Done() //将裁判的1变为0
readyGame.Wait() //保证下一次开始前选手们都已经选好
start.Add(1)
}
fmt.Println(M, " 轮游戏还有", cnt, "个人")
}
func main() {
game1()
}
关于题目的问题我都已经放到代码的注释里面了。
总结:
1.在这次练习中,我感受到了go语言并发机制的魅力,以及极高的运行速度
2.在写代码的过程中我碰到了很多次死锁的问题,以后需要加强这方面的知识储备
3.对sync.waitgroup的理解更加深了一步,它可以用来调节主协程和子协程的运行顺序关系。
谢谢朋友们!如果有什么好的go语言文章欢迎大家分享!
小白真不懂,大哥来照顾!