使用goroutine的方法很简单,直接在语句前面加go关键字即可,如果是多核处理器的电脑,使用gorountine,就会在另外一个CPU上执行goroutine,子协程不一定会和主协程在一个CPU上执行。
这里有两个注意的地方,使用go关键字的进程称之为子协程,而没有使用go关键字的进程称之为主协程,在多CPU的机器上,如果有多个协程,那么这些协程的执行顺序以及执行完成的顺序都是不确定的,但有一点,如果主协程结束,那么整个进程就结束了,不论子协程是否结束,整个进程都结束了,也就看不到子协程的运行结果了。
示例1
package main import "fmt" func main() { go fmt.Println("hello") //子协程 fmt.Println("world") //主协程 }
上面的代码执行之后,会有三种结果:
1、hello world 2、world 3、world hello
上面这三个结果都是正确的。
对于第一种,子协程在一个CPU上面执行输出hello之后,再轮到主协程中执行输出world。
对于第二种,子协程在一个CPU上还没来得及执行,或者说没有执行完成,但是主协程已经执行完成了,此时整个进程就运行完毕了,所以就看不到子协程的运行结果了。
对于第三种,主协程中语句还没执行完成的时候,子协程已经运行完毕,几乎同时完成,所以会出现着这种情况。
可以参考下面这个图来理解:
创建channel
channel的零值是nil
chi := make(chan int) chs := make(chan string) chif := make(chan interface{}) //因为可以认为所有的类型都实现了空接口 //所以这个chif的chan接收任何类型的chan
发送和接收channel
chi := make(chan int) chi <- 3 //发送 i := <-chi //接收,保存值 <-chi //接收,不保存值
关闭channel
chi := make(chan int) chi <- 3 //发送 i := <-chi //接收,保存值 <-chi //接收,不保存值 close(chi) //关闭channel
对一个已经关闭的channel发送数据,都会导致panic异常。对于一个已经关闭的channel,可以继续接收这个已关闭的channel中的数据,如果没有数据的话,接收到的就是chan类型的零值。
无缓存channel(同步channel)
一个基于无缓存的channel:
1、发送操作将导致发送者的gorountine阻塞,直到有两外一个goroutine在相同的channel上执行接收操作之后,发送者的阻塞才会解开。
2、同理,如果接收操作先发生,那么接受者goroutine也将阻塞,直到另一个goroutine在相同的channel上执行发送操作,接受者的阻塞才会解开。
基于无缓存的channel的发送和接收操作将导致两个goroutine做一次同步操作,所以也叫作同步channel。
向一个无缓冲的channel中发送多次数据,即多个goroutine都向同一个goroutine中发送数据,并且只有一个协程来接收数据,这个时候会后什么问题吗?会发生覆盖?或者是只有第一个发送的值才会保留,后面发送的数据都无效?和有缓存channel有什么区别?
请看下面的示例:
package main import "fmt" func main() { chi := make(chan int) //无缓冲 go func() { chi <- 10 chi <- 30 chi <- 40 }() // fmt.Println(<-chi) //10 // fmt.Println(<-chi, <-chi) //10 30 //fmt.Println(<-chi, <-chi, <-chi)//10 30 40 fmt.Println(<-chi, <-chi, <-chi, <-chi) //deadlock }
从上面的例子中可以看到,有一个gorountine向一个无缓冲的channel中发送三个数据,此时,分这几种情况:
0、chi还没有来得及向channel中发送数据时,主协程阻塞,子协程可以向其中发送一个数据。
1、 主协程只从channel中接收一次数据:子协程向chi中发送了一个数据,此时,自协程阻塞,等待chi中的数据被消费之后,才继续运行。 那么,主协程接收的第一个数据就是第一次向这个channel中发送的数据10,之后,channel空闲,发送者可以发送第二个数据30,子协程再次阻塞,等待主协程接收数据,但是此时主协程不接受数据,主协程已经执行完成了,那么整个进程就完成了,所以最终只输出10这一个数据。此时子协程也会跟着被结束。
2、 主协程只从channel中接收两次数据:接着上一步,此时主协程第二次接收数据的话,接收到的是30;之后,chi空闲,子协程再次向chi中发送数据40,然后又阻塞,但是此时主协程已经完成,所以子协程再次被结束。
3、主协程从channel中接收三次数据:接着上一步,此时主协程接收到的数据应该是40,之后,chi空闲,子协程也不会像其中发送数据了,同时,子协程也已经运行结束了,所以,主协程中输出10 30 40。
4、主协程从channel中接收四次数据:接着上一步,因为chi已经空闲,且子协程已经结束,表示没有协程向chi中发送数据了,但是chi并没有关闭,那么主协程中第四次的接收数据就会一直等待,最终造成死锁。解决这个问题,可以在子协程中发送第三次数据40之后,执行close(chi),之后,主协程第四次接收数据便不会引起死锁,接收到的数据时0(chi的类型零值)
使用range遍历channel
package main import ( "fmt" ) func main() { num := make(chan int) go func() { num <- 30 num <- 40 close(num) }() for x := range num { fmt.Println(x) } //输出 //30 //40 }
使用range遍历channel时,从channel中接收数据,如果从channel中获取到了数据,那么返回的结果就是获取到的值;如果channel已经关闭,那么对于无缓冲的channel而言,就没有数据可获取了,但是range的结果是channel的类型零值。
串联channel
下面是一个串联的channel,实现
package main import ( "fmt" "time" ) func main() { num := make(chan int) sqrt := make(chan int) go func() { for x := 0; ; x++ { num <- x time.Sleep(time.Second) } }() go func() { for { x := <-num sqrt <- x * x } }() for { fmt.Println(<-sqrt) } }
上面这个代码中,存在问题:如果第一个子协程不再往num中发送数据,那么,第二个子协程就会阻塞,同理,主协程sqrt取数据也会阻塞,最终引发死锁。
解决方法:
package main import ( "fmt" "time" ) func main() { num := make(chan int) sqrt := make(chan int) go func() { for x := 0; x < 10; x++ { num <- x time.Sleep(time.Second) } //不再向num中发送数据时,关闭channel,防止死锁 close(num) }() go func() { for x := range num { sqrt <- x * x } //num已经关闭了,所以也要关闭sqrt close(sqrt) }() for x := range sqrt { fmt.Println(x) } }
单向channel
使用channel作为参数进行通信时,在函数内部,有的channel只接受数据,有的channel只发送数据,这是可以使用单向的channel表示。
双向的channel:因为关闭channel操作 一般是 发送方通知接收方,发送方不再向channel中发送新的数据了,所以通常只有在发送者所在的goroutine才会调用close函数,在接收channel数据的goroutine中关闭channel显然是不合理的,很有可能会引起错误,因为发送数据的goroutine可能不知道channel已经关闭了,所以他一旦向channle中发送数据,就会引发错误。
单向channel:只有发送者的goroutine中可以调用close函数来关闭单项channel。如果一个goroutine中,对一个只接受的channel调用close函数,或引发编译错误。
任何双向的channel向单向的channel赋值都将导致隐式转换,但是不能反向转换。
声明单向channle
chi := make(<-chan int) //只能被读数据的channel chi := make(chan <- int) //只接收数据的channel
有缓冲的channel
chi := make(chan int) //无缓冲 chii := make(chan int,10) //有缓存channel,缓存空间可存10个值
定义了一个有缓存的channel之后:
在往channel中发数据的时候,只有当channel的缓冲区数量不为0时,数据才能写入,写数据的goroutine才不会阻塞。
在从channel中读数据的时候,只有当channel不为空的时候,读channel的goroutine才不会阻塞。
package main import ( "fmt" "time" ) func WriteData(chi chan int) { go func() { for x := 0; ; x++ { chi <- x time.Sleep(2 * time.Second) } }() go func() { for x := 0; ; x++ { chi <- x time.Sleep(time.Second) } }() go func() { for x := 0; ; x++ { chi <- x time.Sleep(2 * time.Second) } }() } func main() { chi := make(chan int, 3) WriteData(chi) for x := range chi { fmt.Println(x) } }
此例中,如果写数据时channel已满,那么写数据的goroutine会阻塞。如果读channel时,channel为空,那么读数据的goroutine会阻塞。
并发中的循环
package main import "fmt" func loop() { for x := 0; x < 10; x++ { go func() { fmt.Println(x) }() } } func main() { loop() }
上面这个代码执行的时候,可能有以下问题:
1、没有输出 2、只输出一部分值 3、输出很多相同的值 4、输出的值全相同
对于1、2、3的问题,是因为主协程执行速度太快,各个goroutine还没来得及执行,主协程就结束了,于是整个进程就结束了,也就看不到输出了。
解决方法:在主协程中加一个休眠,给各个协程时间去执行。
package main import ( "fmt" "time" ) func loop() { for x := 0; x < 10; x++ { go func() { fmt.Println(x) }() } } func main() { loop() time.Sleep(time.Second) }
对于第四个问题,是因为loop中的循环太快,而各个goroutine太慢,于是所有goroutine在执行前,循环已经完毕,此时i为10,于是全部都打印10了。
解决方法:将i作为参数(闭包)
package main import ( "fmt" "time" ) func loop() { for x := 0; x < 10; x++ { go func(i int) { //i为形参 fmt.Println(i) }(x) //x为实参 } } func main() { loop() time.Sleep(time.Second) }
select接收channel
package main import ( "fmt" ) func loop(ch chan int, chi chan int) { go func() { for { ch <- 9 } }() go func() { for { chi <- 8 } }() } func main() { ch, chi := make(chan int), make(chan int) loop(ch, chi) select { case <-ch: fmt.Println("接收到ch中的数据") case <-chi: fmt.Println("接收到chi中的数据") } }
select类似于if else,每一个case表示接收到channel的数据,执行对应的操作。只不过上面这个例子中,只会接收一次,要么接收的是ch,要么是chi,之后就不会再从channel中读数据了。
如果要一直等待接收多个channel,即,要接收多个channel中的数据,可以使用for + select即可:
package main import ( "fmt" "time" ) func loop(ch chan int, chi chan int) { go func() { for { ch <- 9 time.Sleep(2 * time.Second) } }() go func() { for { chi <- 8 time.Sleep(3 * time.Second) } }() } func main() { ch, chi := make(chan int), make(chan int) loop(ch, chi) for { select { case <-ch: fmt.Println("接收到ch中的数据") case <-chi: fmt.Println("接收到chi中的数据") } } }
并发的退出
package main import ( "fmt" "time" ) func main() { exit := make(chan bool) for x := 1; x < 5; x++ { go func(i int) { for { select { case <-exit: fmt.Println("第", i, "个goroutine结束运行") return default: fmt.Println("第", i, "个goroutine正在运行") } time.Sleep(time.Second) } }(x) } time.Sleep(time.Second * 5) //各个goroutine运行 close(exit) //广播结束消息 time.Sleep(time.Second * 1) //等待各个goroutine打印结束信息 }
有疑问加站长微信联系(非本文作者)