go语言基础(并发--goroutine+channel)
一、Go协程
1、Go协程和Go主线程
1)Go主线程(有程序员直接称为线程/也可以理解成进程):一个Go线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。
2)Go协程的特点
有独立的栈空间
共享程序堆空间
调度由用户控制
协程是轻量级的线程9o线程-协程
2、案例说明
请编写一个程序,完成如下功能:
1)在主线程(可以理解成进程)中,开启一个goroutine,该协程每隔1秒输出"hello,world"
2)在主线程中也每隔一秒输出“hello,golang"”,输出10次后,退出程序
3)要求主线程和goroutine同时执行.
4)画出主线程和协程执行流程图
package main import( "fmt" "strconv" "time" ) func test(){ fori :=1; i<=10; i++{ fmt.Println("tesst() hello,world"+strconv.rtoa(i)) time.sleep(time.second) } func main(){ go test() ∥开启了一个协程 for i :=1; i<=10; i++{ fmt.Print1n("main() he11o,golang"+strconv.rtoa(i)) time.sleep(time.second) } }
程序关系示例图
3、协程小结
1)主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu 资源。
2)协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3)Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了
二、goroutine调度模型
1、MGP模式
1)M:操作系统的主线程(是物理线程)
2)P:协程执行需要的上下文
3)G:协程
2、模型示例图,简图
三、设置CPU数目
1、介绍:为了充分了利用多cpu的优势,在Golang程序中,设置运行的cpu数目。
1)go1.8后,默认让程序运行在多个核上,可以不用设置了
2)go1.8前,还是要设置一下,可以更高效的利益cpu
package main import "fmt" import "runtime" func main(){ //获取当前系统cpu的数量 num := runtime.NumcPU() //我这里设置num-1的cpu运行go程序 runtime.GOMAXPROCS(num) fmt.Println("num=", num) }
四、不同goroutine之间如何通讯
1、goroutine之间通讯资源争夺解决方式
1)全局变量的互斥锁
2)使用管道channel来解决
2、使用全局变量加锁同步改进程序
1)因为没有对全局变量m加锁,因此会出现资源争夺问题,代码会出现错误,提示concurrent map writes
2)解决方案:加入互斥锁
package main import ( "fmt", "runtime", "sync" ) //需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。 //最后显示出来。要求使用goroutine完成 //思路 //1.编写一个函数,来计算各个数的阶乘,并放入到map中. //2.我们启动的协程多个,统计的将结果放入到map中 //3.map应该做出一个全局的。 var( myMap = make(map[int] int, 10) //声明一个全局的互斥锁 //1ock 是一个全局的互斥锁, //sync是包:synchornized同步 //Mutex:是互斥 lock sync.Mutex ) //test函数就是计算n!,让将这个结果放入到myMap func test(n int){ res := 1 for i :=1; i<=n; i++{ res *= i ∥这里我们将res放入到myMap //加锁 1ock.Lock() myMap[n] = res //concurrent map writes? //解锁 1ock.Unlock() } func main(){ //我们这里开启多个协程完成这个任务[200个] for i := 1; i <= 200; i++{ go test(i) } //休眠5秒钟【第二个问题】 time.sleep(time.Second*5) //这里我们输出结果,变量这个结果 1ock.Lock() for i,v :=range myMap{ fmt.printf("map[%d]=%d\n",i,v) } 1ock.unlock() }
五、管道(channel)
1、为什么需要channel
1)前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
2)主线程在等待所有goroutine全部完成的时间很难确定,我们这里设置10秒,仅仅是估算。
3)如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
4)通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
5)上面种种分析都在呼唤一个新的通讯机制-channel
2、channel的介绍
1)channle本质就是一个数据结构-列【示意图】
2)数据是先进先出
3)线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
4)channel时有类型的,一个string的channel只能存放string类型数据。
3、定义/声明channel
1)channel是引用类型
2)channel必须初始化才能写入数据,即make后才能使用
3)管道是有类型的,intChan只能写入整数int
var 变量名 chan 数据类型 //举例: var intChan chan int //(intChan用于存放int数据) var mapChan chan map[int]string //(mapChan用于存放map[int]string类型) var perChan chan Person // 结构体Person var perChan2 chan *Person // 结构体指针
package main import( "fmt” ) func main() { //演示一下管道的使用 //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 := 211 intchan<- num intchan<- 50 //intchan<-98 //注意点,当我们给管写入数据时,不能超过其容量 //4.看看管道的长度和cap(容量) fmt.Printf("channel 1en=%/cap=%v\n",1en(intchan),cap(intchan)) //3,3 //5.从管道中读取数据 var num2 int num2 = <-intchan fmt.Println("num2=",num2) fmt.Printf("channel 1en=%v cap=%v\n", 1en(intchan),cap(intchan)) //2,3 //6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报错 deadlock num3 := <-intChan num4 := <-intChan num5 := <-intChan fmt.Println("num3=", num3,"num4=", num4,"num5=", num5) }
4、代码示例,管道放入map
创建一个mapChan,最多可以存放10个map[stringlstring的key-val,演示写入和读取。
func main(){ var mapchan chan map[string]string mapChan = make(chan map[string]string, 10) m1 :=make(map[string]string, 20) ml["city1"]="北京" ml["city2"]="天津" m2 := make(map[string]string, 20) m2["hero1"]="宋江" m2["hero2"]="武松” //.. mapchan<-m1 mapChan<-m2 }
5、代码示例,管道放入结构体
创建一个catChan,最多可以存放10个Cat结构体变量,演示写入和读取的用法
func main(){ var catchan chan cat catchan = make(chan cat, 10) cat1 := cat{Name:"tom", Age:18} cat2 := cat{Name:"tom~", Age:19} catchan<- cat1 catchan<- cat2 //取出 cat11 := <-catchan cat22 := <-catchan fmt.Println(cat11,cat22) }
6、代码示例,管道放入指针
创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的用法
func main(){ var catchan chan *cat catchan=make(chan *cat, 10) cat1 := cat{Name:"tom", Age:18} cat2 := cat{Name:"tom~", Age:19} catchan<- &cat1 catchan<- &kat2 //取出 cat11 := <-catchan cat22 := <-catchan fmt.Printin(cat11, cat22) }
7、代码示例,管道放入任意类型数据
创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
func main(){ var allchan chan interface{} allchan = make(chan interface{}, 10) cat1:=cat{Name:"tom", Age:18} cat2:=cat{Name:"tom~", Age:180} allchan<- cat1 allchan<- cat2 allchan<- 10 allchan<- "jack" //取出 cat11 := <-allchan cat22 := <-allchan V1 := <-allchan v2 := <-allchanl fmt.Println(cat11, cat22, v1, v2) }
8、代码示例,管道放入interface处理
通过管道获取interface类型时,需要进行类型断言
func main(){ //定义一个存放任意数据类型的管道3个数据 //var allchan chan interface{} allchan := make(chan interface{}, 3) allchan<- 10 allchan<- "tom jack" cat := cat{"小花猫", 4} allchan<- cat //我们希望获得到管道中的第三个元素,则先将前2个推出 <-allchan <-allchan newcat := <-allchan //从管道中取出的cat是什么? fmt.Printf("newcat=%T,newcat=%v\n", newcat, newcat) //下面的写法是错误的!编译不通过 //fmt.Printf("newcat.Name=%v",newcat.Name) //使用类型断言 a := newcat.(cat) fmt.Printf("newcat.Name=%v", a.Name) }
10、channel使用的注意事项
1)channel中只能存放指定的数据类型
2)channle的数据放满后,就不能再放入了
3)如果从channel取出数据后,可以继续放入
4)在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
六、channel的遍历和关闭
1、channel的关闭
使用内置函数close可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据。
2、代码示例,管道关闭
package main import( "fmt" ) func main(){ intchan := make(chan int, 3) intchan<- 100 intchan<- 200 close(intchan) //close channel //这是不能够再写入数到channe1 //intchan<-300 fmt.Println("okook~") //当管道关闭后,读取数据是可以的 n1 := <-intchan fmt.Print1n("n1=", n1) }
3、channel的遍历
channel支持for--range的方式进行遍历,请注意两个细节
1)在遍历时,如果channel没有关闭,则回出现deadlock的错误
2)在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
package main import( "fmt" ) func main(){ //遍历管道 intchan2 := make(chan int, 100) for i :=0; i<100; i++{ intchan2<- i*2 //放入100个数据到管道 } //遍历管道不能使用普通的for循环 //管道长度会改变 //for i:=e;i< len(intchan2);i++{ //} //在遍历时,如果channe1没有关闭,则会出现deadlock的错误 //在遍历时,如果channe1已经关闭,则会正常遍历数据,遍历完后,就会退出遍历 Close(intchan2) for v := range intchan2{ fmt.Println("v=", v) } }
4、案例
请完成goroutine和channel协同工作的案例,具体要求:
1)开启一个writeData协程,向管道intChan中写入50个整数.
2)开启一个readData协程,从管道intChan中读取writeData写入的数据。
3)注意:writeData和readDate操作的是同一个管道
4)主线程需要等待writeData和readDate协程都完成工作才能退出
package main import( "fmt", _"time" ) //write Data func writeData(intchan chan int){ fori :=1; i<=50; i++{ //放入数据 intchan<- i fmt.Println("writeData", i) time.sleep(time.second) c1ose(intchan) //关闭管道 } //read data func readData(intchan chan int, exitchan chan bool){ for{ v, ok := <-intchan // 管道为空时退出 if !ok{ break } fmt.Printf("readpata 读到数据=%\n", v) //readData 读取完数据后,即任务完成 exitchan<- true close(exitchan) } } func main(){ //创建两个管道 intchan := make(chan int, 50) exitchan := make(chan bool, 1) go writepata(intchan) go readpata(intchan, exitchan) //time.sleep(time.second*10) for{ _, ok := <-exitchan if !ok{ break } } }
5.案例
需求:要求统计1-8000的数字中,哪些是素数?
package main import( "fmt" ) //向intchan放入1-8000个数 func putNum(intchan chan int){ for i :=1; i<=80; i++{ intchan<- i } //关闭intchan close(intchan) } //从intchan取出数据,并判断是否为素数,如果是,就 //放入到primechan func primeNum(intchan chan int, primechan chan int, exitchan chan bool){ //使用for循环 //var num int 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.Print1n("有一个primeNum协程因为取不到数据,退出") //这里我们还不能关闭primechan //向exitchan写入true exitchan<- true } func main(){ intchan := make(chan int, 1000) primechan := make(chan int, 2000) //放入的是素数 //标识退出的管道 exitchan := make(chan bool, 4) //4个处理素数的协程标志 //开启一个协程,向intchan放入1-8000个数 go putNum(intchan) //开启4个协程,从intchan取出数据,并判断是否为素数,如果是,就放入到primechan for i := 1; i<4; i++{ go primeNum(intchan, primechan, exitchan) } //这里我们主线程,进行处理 //直接,匿名函数 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.Print1n("main线程退出") }
七、channel使用细节和注意事项
1)channel可以声明为只读,或者只写性质
2)channel只读和只写的最佳实践案例
package main import( "fmt" ) func main(){ //管道可以声明为只读或者只写 //1.在默认情况下下,管道是双向 var chanl chan int //可读可写 //2.声明为只写 var chan2 chan<- int chan2=make(chan int, 3) chan2<- 20 //num:=<-chan2 //error fmt.Printin("chan2=",chan2) //3.声明为只读 var chan3 <-chan int num2 := <-chan3 //chan3<-30 //error fmt.Print1n("num2",num2) }
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方式可以解决 //1abe1: 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("都取不到了,不玩了,程序员可以加入逻辑\n") time.s1eep(time.Second) return // 退出for //break labe1 //退出到指定label } } }
4)goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题.
package main import( "fmt", "time" ) //函数 func sayHel1o(){ for i := 0; i<10; i++{ time.sleep(time.second) fmt.Println("he11o,world") } //函数 func test(){ //这里我们可以使用defer+recover defer func(){ //捕获test抛出的panic if err := recover(); err != nil{ fmt.Println("test()发生错误", err) } }() //定义了一个map var myMap map[int]string myMap[0]="golang" //这里模拟error,因为map需要make初始化 } func main(){ go sayHello() go test() for i := 0; i<10; i++{ fmt.Println("main() ok=", i) time.sleep(time.Second) } }