Go的学习旅程6:并发与通信
1.并发
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go
关键字实现了,其实就是一个普通的函数。
package main import ( "fmt" "runtime" "time" ) func say(s string){ for i:=0;i<10 ;i++ { //runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。 runtime.Gosched() fmt.Println(s) } } func main() { //并行执行 for i:=0;i<10 ;i++ { go func(i int) { fmt.Println("我在并发执行:",i) }(i) } //休息1秒 time.Sleep(time.Millisecond*1000) //普通执行 for i:=0;i<10 ;i++ { fmt.Println("我在并发执行:",i) } go say("你好") say("hello") }
结果展示:
2.通信
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel.
所有的通信,都是基于并发也就是goroutine来做的
2.1 Range,Close以及channel的基础用法
package main import ( "fmt" "time" ) //简单的写法 func aaa() { //创建channel(用于协程的通讯) a := make(chan int) go func() { for c := range a { //channel通过操作符<-来接收和发送数据 //c接收a的消息 fmt.Println(c) } }() //channel通过操作符<-来接收和发送数据 //发送1,2,3到a a <- 1 a <- 2 a <- 3 time.Sleep(time.Millisecond * 500) } //封装的写法 func bbb() { a := make(chan int) go worker(a) a <- 4 a <- 5 a <- 6 close(a) time.Sleep(time.Millisecond * 500) } func worker(c chan int) { //判断剔除空数据 for n := range c { fmt.Println(n) } } //创建多个通讯 func ccc() { var a [10]chan int for i := 0; i < 10; i++ { a[i] = workk(i) } for i := 0; i < 10; i++ { a[i] <- 'a' + i //发送方给接收方断开,close的发送值是空,需要在接收方里面判断是否为空 close(a[i]) } time.Sleep(time.Millisecond) } func workk(i int) chan int { c := make(chan int) //方法一 //go func() { // if n, ok := <-c; ok { // fmt.Printf("id是: %d,通讯是: %c \n", i, n) // } //}() //方法二 go func() { for n := range c { fmt.Printf("id是: %d,通讯是: %c \n", i, n) } }() return c } func main() { //aaa() //bbb() //ccc() }
1. 创建了一个无缓冲的channel
2. 主routine要向channel中放入一个数据,但是因为channel没有缓冲,相当于channel一直都是满的,所以这里会发生阻塞。可是下面的那个goroutine还没有创建呢,主routine在这里一阻塞,整个程序就只能这么一直阻塞下去了,然后。。。然后就没有然后了。。死锁!
※从这里可以看出,对于无缓冲的channel,放入操作和取出操作不能再同一个routine中,而且应该是先确保有某个routine对它执行取出操作,然后才能在另一个routine中执行放入操作。
func bbb() { a := make(chan int) a <- 4 a <- 5 a <- 6
go worker(a)close(a) time.Sleep(time.Millisecond * 500)}
结果展示:
2.2 channel任务的结束
1.利用通信,完成任务的回调
2.利用sync包来解决协程间的同步与通信
package main import ( "fmt" "sync" ) type work struct { i chan int dn chan bool } //创建多个通讯,利用通信来共享内存 func ccc() { var a [10]work for i := 0; i < 10; i++ { a[i] = workk(i) } for i, v := range a { v.i <- 'a' + i // 取出数据 (通过 <-channel 操作) //<-a[i].dn //返回true } for i, v := range a { // 取出数据 (通过 <-channel 操作) <-v.dn fmt.Printf("我的编号是%d \n", i) } for i, v := range a { v.i <- 'A' + i // 取出数据 (通过 <-channel 操作) //<-a[i].dn //返回true } for i, v := range a { // 取出数据 (通过 <-channel 操作) <-v.dn fmt.Printf("我的编号是%d \n", i) } } func workk(i int) work { w := work{ i: make(chan int), dn: make(chan bool), } go func() { for n := range w.i { fmt.Printf("id是: %d,通讯是: %c \n", i, n) //放入数据,如果两个chan一起写报错,原因是w.dn <- true被阻塞,没有消耗 w.dn <- true //go func() {w.dn <- true}() } }() return w } //利用go包中的WaitGroup实现队列 type wg_sync struct { i chan int wg *sync.WaitGroup } func ddd() { var wg sync.WaitGroup var a [10]wg_sync wg.Add(20) for i := 0; i < 10; i++ { a[i] = wg_work(i, &wg) } for i, v := range a { v.i <- 'a' + i // 取出数据 (通过 <-channel 操作) //<-a[i].dn //返回true } for i, v := range a { v.i <- 'A' + i // 取出数据 (通过 <-channel 操作) //<-a[i].dn //返回true } wg.Wait() } func wg_work(i int, wg *sync.WaitGroup) wg_sync { w := wg_sync{ i: make(chan int), wg: wg, } go func() { for n := range w.i { fmt.Printf("id是: %d,通讯是: %c \n", i, n) w.wg.Done() } }() return w } func main() { //fmt.Println("我是ccc") //ccc() fmt.Println("我是ddd") ddd() }
结果展示: