关于go并发的实例及理解

本文围绕GO的并发操作展开,作者应学长要求解答题目,用GO并发编写代码,起初代码逻辑有问题,后用sync包的waitgroup优化。学长指出代码复杂,给出自己的代码。作者总结此次练习,感受GO并发魅力,意识到死锁问题,加深了对sync.waitgroup的理解。

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语言文章欢迎大家分享!
小白真不懂,大哥来照顾!

本书作者带你一步一步深入这些方法。你将理解 Go语言为何选定这些并发模型,这些模型又会带来什么问题,以及你如何组合利用这些模型中的原语去解决问题。学习那些让你在独立且自信的编写与实现任何规模并发系统时所需要用到的技巧和工具。 理解Go语言如何解决并发难以编写正确这一根本问题。 学习并发与并行的关键性区别。 深入到Go语言的内存同步原语。 利用这些模式中的原语编写可维护的并发代码。 将模式组合成为一系列的实践,使你能够编写大规模的分布式系统。 学习 goroutine 背后的复杂性,以及Go语言的运行时如何将所有东西连接在一起。 作者简介 · · · · · · Katherine Cox-Buday是一名计算机科学家,目前工作于 Simple online banking。她的业余爱好包括软件工程、创作、Go 语言(igo、baduk、weiquei) 以及音乐,这些都是她长期的追求,并且有着不同层面的贡献。 目录 · · · · · · 前言 1 第1章 并发概述 9 摩尔定律,Web Scale和我们所陷入的混乱 10 为什么并发很难? 12 竞争条件 13 原子性 15 内存访问同步 17 死锁、活锁和饥饿 20 确定并发安全 28 面对复杂性的简单性 31 第2章 对你的代码建模:通信顺序进程 33 并发与并行的区别 33 什么是CSP 37 如何帮助你 40 Go语言的并发哲学 43 第3章 Go语言并发组件 47 goroutine 47 sync包 58 WaitGroup 58 互斥锁和读写锁 60 cond 64 once 69 池 71 channel 76 select 语句 92 GOMAXPROCS控制 97 小结 98 第4章 Go语言的并发模式 99 约束 99 for-select循环103 防止goroutine泄漏 104 or-channel 109 错误处理112 pipeline 116 构建pipeline的最佳实践 120 一些便利的生成器 126 扇入,扇出 132 or-done-channel 137 tee-channel 139 桥接channel模式 140 队列排队143 context包 151 小结 168 第5章 大规模并发 169 异常传递169 超时和取消 178 心跳 184 复制请求197 速率限制199 治愈异常的goroutine 215 小结 222 第6章 goroutine和Go语言运行时 223 工作窃取223 窃取任务还是续体 231 向开发人员展示所有这些信息 240 尾声 240 附录A 241
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值