需求,现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。
要求使用goroutine完成
➢分析思路:
1) 使用goroutine来完成,效率高,但是会出现并发/并行安全问题.
2) 这里就提出了不同goroutine如何通信的问题
➢代码实现
1) 使用goroutine来完成(看看使用gorotine 并发完成会出现什么问题?然后我们会去解决)
2) 在运行某个程序时,如何知道是否存在资源竞争问题。方法很简单, 在编译该程序时,增加一
个参数-race 即可
go build -race main.go
3)示意图:
不同goroutine之间如何通讯
1.全局变量加锁同步
2. channel
1.使用全局变量加锁同步改进程序
1)因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示
concurrent map writes
2) 解决方案: 加入互斥锁
3) 我们的数的阶乘很大, 结果会越界
在实际运行中,还是可能在红框部分出现( 运行时增加-race参数,确实会发现有资源竞争问题),主线程并不知道,协程已经执行完成,因此底层可能仍然出现资源争夺,因此加入互斥锁即可解决问题。
4)代码
package main
import (
"fmt"
"sync"
)
//1.编写一个函数,来计算各个数的阶乘,并放入到map中.
//2.我们启动的协程多个,统计的将结果放入到map中
//3.map应该做出一个全局的.
var (
myMap = make(map[int]int,10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包 : synchornized 同步
//Mutex : 互斥
lock sync.Mutex
)
func test(n int){
//求阶乘
res := 1
for i := 1;i<=n;i++{
res *= i
}
//放入到myMap
//加锁
lock.Lock()
myMap[n]=res // fatal error: concurrent map writes
//解锁
lock.Unlock()
}
func main(){
//开起了200个协程完成这个任务
for i := 1;i<=20;i++{
go test(i)
}
//输出结果
//加锁
lock.Lock()
for i,v := range myMap{
fmt.Printf("map[%d]=%d\n",i,v)
}
//解锁
lock.Unlock()
}
channel
为什么需要channel
前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
1) 主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。
2) 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
3) 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
channel的介绍
1) channle本质就是一个数据结构-队列
2) 数据是先进先出
3) 线程安全,多goroutine访问时, 不需要加锁,就是说channel本身就是线程安全的
4) channel时 有类型的,一个sring的channel只能存放string类型数据。
定义/声明channel
var 变量名 chan 数据类型
举例:
var intChan chan int (intChan用于存放int数据)
var mapChan chan map[int]string (mapChan用于存放map[int]string类型)
var perChan chan Person
var perChan2 chan *Person
注意:
1) channel是引用 类型
2) channel必须初始化才 能写入数据,即make后才能使用
3) 管道是有 类型的,intChan 只能写入整数int
简单使用
//1.创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int,3)
//2.打印intChan
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n",intChan,&intChan)
//3.先管道写入数据
intChan<- 10
num := 985
intChan<- num
//注意 : 当我们给管道写入数据时,不能超过其容量 会报告deadlock
//4.intChan的容量(cap)和长度
fmt.Printf("intChan len=%v intChan cap=%v\n",len(intChan),cap(intChan)) //2,3
//5.从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=",num2)
fmt.Printf("intChan len=%v intChan cap=%v\n",len(intChan),cap(intChan))
//6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告deadlock
channel使用的注意事项
1)channel中只能存放指定的数据类型
2)channle的数据放满后,就不能再放入了
3)如果从channel取出数据后,可以继续放入
4)在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
channel练习
1) 创建一个mapChan,最多可以存放10个map[string]string的key-val , 演示写入和读取。
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 10)
m1 := make(map[string]string, 20)
m1["city1"] = "北京"
m1["city2"] = "天津"
m2 := make(map[string]string, 20)
m2["hero1"] ="宋江"
m2["hero2"] ="武松"
mapChan <- m1
mapChan <- m2
m3 := <-mapChan
fmt.Printf("m1=%v \n",m3)
2)创建一个allChan,最多可以存放任意数据类型变量,演示写入和读取的用法
//定义一个存放任意数据类型的管道 3个数据
//var allChan chan interface{}
allChan := make(chan interface{},3)
allChan<- 67
allChan<- "mary"
cat := Cat{"tom",6}
allChan<- cat
//我们希望获得管道中第三个元素,则先将前两个2取出
<-allChan
<-allChan
newCat := <-allChan
fmt.Printf("newCat=%T,newCat=%v \n",newCat,newCat)
//下面写法错误
//fmt.Printf("newCat.Name=%v",newCat.Name)
//使用类型断言
a := newCat.(Cat)
fmt.Printf("a=%v",a.Name)
type Cat struct{
Name string
Age int
}
channel的关闭
使用内置函数close可以关闭channel,当channel关闭后, 就不能再向channe|写数据
了,但是仍然可以从该channel读取数据.
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 10)
m1 := make(map[string]string, 20)
m1["city1"] = "北京"
m1["city2"] = "天津"
m2 := make(map[string]string, 20)
m2["hero1"] ="宋江"
m2["hero2"] ="武松"
mapChan <- m1
mapChan <- m2
//关闭管道
close(mapChan)
//读数据
m3 := <-mapChan
fmt.Printf("m1=%v len = %v\n",m3,len(mapChan)) //正常
//测试存放数据 不能存放数据了
//mapChan <- m1 //panic: send on closed channel
channel的遍历
channel支持for-range的方式进行遍历,请注意两个细节
1) 在遍历时,如果channel没有关闭,则会出现deadlock的错误
2) 在遍历时,如果channel已经关闭,则会正常遍历数据,追历完后,就会退出遍历。
//遍历管道
intChan := make(chan int ,100)
for i:=0;i<100;i++{
intChan<- i*2 //放入100个数据放到管道中
}
//关闭管道
close(intChan)
//输出管道的数据
for v := range intChan {
fmt.Println("v=",v)
}
练习
请完成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 := 1;i<=50;i++{
//存放数据
intChan<- i
fmt.Println("writeData data=",i)
}
//关闭管道
close(intChan)
}
//read data
func readData(intChan chan int,exitChan chan bool){
//读数据
for{
v,ok := <-intChan
if !ok {
break
}
fmt.Printf("readData data=%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
}
}
}
如果编译器(运行)发现一个管道只有写,而没有读,则改管道,会阻塞。
写管道和读管道的频率不一致,无所谓。
练习
需求:要求统计1-8000的数字中,哪些是素数?
思路分析
代码
package main
import (
"fmt"
"time"
)
//向 intChan 放入 1-8000个数
func putNum(intChan chan int){
for i:=1;i<= 8000;i++{
intChan<-i
}
//关闭intChan
close(intChan)
}
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
//使用for 循环
var flag bool
for {
num,ok := <-intChan
if !ok { //intChan 取不到了
break
}
flag = true //假定是素数
//判断num是不是素数
for i:=2;i<num;i++{
if num % i == 0{ //说明改num不是素数
flag =false
break
}
}
if flag {
//将这个数放入到primeChan
primeChan<- num
}
}
fmt.Println("有一个协程因为取不到数据,退出了")
//还不能关闭primeChan管道
exitChan<-true
}
func main(){
//需求:要求统计1-200000的数字中,哪些是素数?
intChan := make(chan int ,1000)
primeChan := make(chan int,2000)
//标识退出的管道
exitChan := make(chan bool,4)
//计算开始时间
start := time.Now().Unix()
//开启一个协程,向intChan放入1-8000函数
go putNum(intChan)
//开启4个协程,从intChan取出数据, 并判断是否为素数,如果是,就
//放入到primeChan
for i:=0; i<4 ;i++{
go primeNum(intChan,primeChan,exitChan)
}
//主线程,进行处理
go func(){
//从exitChan取出所有值
for i:=0;i<4;i++{
<-exitChan
}
//结束时间
end := time.Now().Unix()
fmt.Println("使用协程耗时=", end - start)
//当我们从exitchan 取出了4个结果,就可以放心的关闭primeNum
close(primeChan)
}()
//遍历我们的primeNum , 把结果取出
for{
// res,ok := <-primeChan
_,ok := <-primeChan
if !ok{
break
}
//将结果输出
//fmt.Printf("素数=%d\n",res)
}
fmt.Println("main exit")
}
channel使用细节和注意事项
1) channel可以声明为只读,或者只写性质
2) channel只 读和只写的最佳实践案例
3) 使用select可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"time"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道10个数据int
intChan := make(chan int, 10)
for i:=0;i<10;i++{
intChan<- i
}
//2.定义一个管道5个数据string
stringChan := make(chan string,5)
for i :=0;i< 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d",i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致deadlock
//问题, 在实际开发中, 可能我们不好确定 什么关闭该管道.
//可以使用select方式可以解决
for {
select{
//注意:这里,如果intChan-直没有关闭,不会直阻寨 而deadlock
//,会自动到下一个case匹配
case v := <-intChan:
fmt.Printf("从intChan读取到的数据%d\n",v)
time.Sleep(time.Second)
case v := <-stringChan:
fmt.Printf("从stringChan读取到的数据%s\n",v)
time.Sleep(time.Second)
default:
fmt.Printf("game over\n")
time.Sleep(time.Second)
return
}
}
}
4) 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,world")
}
}
func test(){
//捕获panic
defer func(){
//捕获test抛出的panic
if err := recover();err != nil {
fmt.Println("test() 发生错误",err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0]="boys" //error
}
func main(){
go sayHello()
go test()
for i := 0 ;i<10;i++{
fmt.Println("main() ok=",i)
time.Sleep(time.Second)
}
}