案例一
请完成goroutine和channel协同工作的案例,具体要求:
(1).开启一个writeData协程,向管道intChan中写入50个整数.
(2).开启一个readData协程,从管道intChan中读取writeData写入的数据
(3).注意: writeData和readDate操作的是同一个管道
(4).主线程需要等待writeData和readDate协程都完成工作才能退出
示例图如下:
代码如下:
package main
import (
"fmt"
)
//write data
func WriteData(intChan chan int) {
for i := 0; i < 50; i++ {
intChan <- i //放入数据
fmt.Printf("write 数据=%v\n", i)
}
//关闭管道
close(intChan)
}
//read data
func ReadData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Printf("read 读取到的数据=%v\n", v)
}
//读取完成后,任务完成
exitChan <- true
close(exitChan)
}
func main() {
//创建两个管道
intChan := make(chan int, 50)
exitChan := make(chan bool, 1)
go WriteData(intChan)
go ReadData(intChan, exitChan)
//判断
for {
_, ok := <- exitChan
if !ok {
break
}
}
}
阻塞
func main(){ intChan := make(chan int, 10) // 10->50的话数据一下就放入了 exitChan:=make(chan bool,1) go writeData(intChan) go readData(intChan,exitChan) //就是为了等待..readData协程完成 for _ = range exitChan{ fmt.Println("ok...") } }
问题:
如果注销掉 go readData(intChan,exitChan),程序会怎么样?
答:
如果只是向管道写入数据,而没有读取(如果编译器(运行),发现一个管道只有写,而没有读,则该管道会阻塞;写管道和读管道的频率不一致,不影响),就会出现阻塞而dead lock,原因是intChan容量是10,而代码writeData会写入50个数据,因此会阻塞在writeData的 ch<-i
案例二
要求:
(1).启动一个协程,将1~2000的数放入到一个channel中,比如numChan
(2).启动8个协程,从numChan取出数(比如n),并计算1+...+n的值,并存放到resChan
(3).最后8个协呈协同完成工作后,再遍历resChan,显示结果[如res[1]=1..res[10]=55..]
注意:考虑resChan chan int是否合适?
案例三
goroutine+ channel配合完成排序,并写入文件
要求:
(1).开一个协程writeDataToFile,随机生成1000个数据,存放到文件中
(2).当writeDataToFile完成写1000个数据到文件后,让sort协程从文件中读取1000个文件,并完成排序,重新写入到另外一个文件
(3).考察点:协程和管道+文件的综合使用
(4).功能扩展:开10个协程writeDataToFile,每个协程随机生成1000个数据,存放到10文件中
(5).当10个文件都生成了,让10个sort协程从10文件中读取1000个文件,并完成排序,重新写入到10个结果文件
案例四
(1).创建一个 Person 结构体 [Name,Age,Address]
(2).使用rand方法配合随机创建10个Person实例,并放入到channel中
(3).遍历channel,将各个Person实例的信息显示在终端
package main
import (
"fmt"
"math/rand"
"strconv"
)
type Person struct {
Name string
Age int
Address string
}
//创建Person实例,并写入chan
func WriteChan(personChan chan Person) {
for i := 0; i < 100; i++ {
//把person放入管道中
personChan <- Person {
Name : "Person" + strconv.Itoa(rand.Intn(100)),
Age : rand.Intn(100),
Address : "成都市华府大道" + strconv.Itoa(rand.Intn(100)) + "号",
}
fmt.Println("wtite data=", i)
}
//关闭管道
close(personChan)
}
//读取chan数据,并显示到终端
func ReadChan(personChan chan Person, exitChan chan bool) {
i := 0
for {
i++
v , ok := <- personChan
if !ok {
break
}
fmt.Printf("第%v个person数据的Name=%v,Age=%v, Address=%v\n", i,
v.Name, v.Age, v.Address)
}
读取完成后,任务完成
exitChan <- true
close(exitChan)
}
func main() {
//创建两个管道
personChan := make(chan Person, 100)
exitChan := make(chan bool, 1)
go WriteChan(personChan)
go ReadChan(personChan, exitChan)
for {
_, ok := <- exitChan
if !ok {
break
}
}
}
案例五
需求:
要求统计 1 ~ 200000 的数字中,哪些是素数?
分析思路:
传统的方法,就是使用一个循环,循环的判断各个数是不是素数
使用并发并行的方式,将统计素数的任务分配给多个( 4 个) gorontine去完成,完成任务时间短
示意图如下:
代码如下:
package main
import (
"fmt"
"time"
)
//向intChan放入1~8000
func putNum(intChan chan int) {
for i := 1; i <= 1000; i++ {
intChan<- i
}
//关闭intChan
close(intChan)
}
//从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool // 是不是素数标识符
for {
time.Sleep(time.Millisecond * 10)
//从intChan取出数据,并判断是不是素数
num, ok := <-intChan
if !ok {
break
}
flag = true
for i := 2; i < num; i++ {
if num % i == 0 { // 说明不是素数
flag = false
break
}
}
if flag {
//将结果放入primeChan
primeChan<- num
}
}
fmt.Println("有一个primeChan取不到数据,退出")
//在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
//向exitChan放入true
exitChan<- true
}
func main() {
//创建3个管道
intChan := make(chan int, 1000) //存放1~8000的数
primeChan := make(chan int, 1000) //放入结果
exitChan := make(chan bool, 4) //标识退出的管道
//开启一个协程,向intChan放入1~8000个数
go putNum(intChan)
//开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
for i := 1; i <= 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//判断primeChan是否执行完毕,并关闭prizeChan
go func(){
for i := 1; i <= 4; i++ {
<-exitChan
}
//当从exitChan中取出4个数后,就可以关闭primeChan了
close(primeChan)
}()
//遍历primeChan
for {
res, ok := <-primeChan
if !ok {
break
}
//输出
fmt.Printf("素数:%d\n", res)
}
fmt.Println("main主线程退出")
}
看看执行的速度:
package main
import (
"fmt"
"time"
)
//向intChan放入1~8000
func putNum(intChan chan int) {
for i := 1; i <= 80000; i++ {
intChan<- i
}
//关闭intChan
close(intChan)
}
//从intChan取出并判断是否素数,如果是,就放入primeChan中,当前协程完成后,需设置exitChan退出标识
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool // 是不是素数标识符
for {
// time.Sleep(time.Millisecond * 10)
//从intChan取出数据,并判断是不是素数
num, ok := <-intChan
if !ok {
break
}
flag = true
for i := 2; i < num; i++ {
if num % i == 0 { // 说明不是素数
flag = false
break
}
}
if flag {
//将结果放入primeChan
primeChan<- num
}
}
fmt.Println("有一个primeChan取不到数据,退出")
//在这里还不能关闭primeChan,因为不知道哪一个primeChan还在执行
//向exitChan放入true
exitChan<- true
}
func main() {
//创建3个管道
intChan := make(chan int, 1000) //存放1~8000的数
primeChan := make(chan int, 1000) //放入结果
exitChan := make(chan bool, 4) //标识退出的管道
//开始时间
start := time.Now().Unix()
//开启一个协程,向intChan放入1~8000个数
go putNum(intChan)
//开启4个协程,从intChan取出并判断是否素数,如果是,就放入primeChan中
for i := 1; i <= 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//判断primeChan是否执行完毕,并关闭prizeChan
go func(){
for i := 1; i <= 4; i++ {
<-exitChan
}
//结束时间
end := time.Now().Unix()
fmt.Println("使用协程耗费的时间:", end - start)
//当从exitChan中取出4个数后,就可以关闭primeChan了
close(primeChan)
}()
//遍历primeChan
for {
_, ok := <-primeChan
if !ok {
break
}
//输出
//fmt.Printf("素数:%d\n", res)
}
fmt.Println("main主线程退出")
}
结论:使用go协程后,执行速度比普通方法提高至少4倍
注意事项
1.管道可以声明为只读或只写
package main
import (
"fmt"
)
func main() {
//管道可以声明为只读或只写
//1.在默认的情况下,管道是双向的
// var chan1 chan int//可读可写
//2.声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 33
// num := <-chan2 //error
fmt.Println("chan2=", chan2)
//2.声明为只读
var chan3 <-chan int
chan3 = make(chan int, 3)
num := <-chan3
//chan3<- 33//error
fmt.Println("num=", num)
}
2.使用select可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}
//2.定义一个管道 5个数据string
strChan := make(chan string, 5)
for i := 0; i < 5; i++ {
strChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方式在遍历管道时,如果不关闭会阻塞而导致deallock
//问题:在实际开发中,有可能不好确定在什么情况下去关闭管道
//可以使用select方式解决
for {
select {
//注意,如果intChan一直没有关闭,不会一直阻塞而deallock,会自动到下一个case匹配
case v := <- intChan :
fmt.Printf("从intChan取出数据:%v\n", v)
case v := <- strChan :
fmt.Printf("从strChan取出数据:%s\n", v)
default :
fmt.Println("取不到数据了,程序员可以添加自己的逻辑代码")
return
}
}
}
3.goroutine 中使用 recover 解决协程中出现 panic导致程序崩溃问题
如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成整个程序崩溃,这时我们可以在 goroutine 中使用 recover来捕获 panic ,进行处理,这样即使这个协程发生的问题,但是主线程仍然不受影响,可以继续执行
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello")
}
}
func test() {
//这里可以使用defer + recover
defer func () {
//捕获test抛出的异常
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
var map1 map[int]string
map1[0] = "go" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main hi")
time.Sleep(time.Second)
}
}
[上一节][go学习笔记.第十四章.协程和管道] 2.管道
[下一节]go学习笔记.第十五章.反射] 1.反射的基本介绍以及实践