单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel是一种类型,一种引用类型。
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
向管道发送数据
ch <- 10 // 把10发送到ch中
从管道接受数据
x := <- ch // 从ch中接收值并赋值给变量x
//<-ch // 从ch中接收值,忽略结果
关闭管道
close(ch)
关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。
关闭后的通道有以下特点:
妈妈知道我学习累了,允许我玩半小时游戏。半小时之后她通过channel告诉我,你该睡觉了。
女朋友在和我进行了十个来回的对话之后,发现了可能是机器人,她不再回复我了。
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
//定义一个计数器对goroutine进行计数,main函数结束前,保证其它goroutine都已经执行结束
var wg sync.WaitGroup
func chat(who string) {
defer wg.Done()
for i := 0; i < 10; i++ { //女朋友聊十句话,发现了可能是机器人,睡觉了
n := rand.Int31n(5) //生成[0,5)的随机数
switch n {
case 0:
fmt.Println(who, ",我想你了,你在干嘛?")
case 1:
fmt.Println(who, ",我想你了,我在和小可爱聊天呀")
case 2:
fmt.Println(who, ",你今天有什么有趣的跟我分享吗?")
case 3:
fmt.Println(who, ",哇啊,还是我的小可爱机智过人,比心")
case 4:
fmt.Println(who, ",亲爱的,你真漂亮")
default:
fmt.Println(who, ",I love you!!!")
}
time.Sleep(time.Second)
}
}
func countdown(chWithMa chan bool) {
defer wg.Done() //告诉计数器,我的任务完成了,我要退出了
time.Sleep(time.Minute)
chWithMa <- false
close(chWithMa)
}
func main() {
who := "girlFriend"
chWithMa := make(chan bool)
wg.Add(2)
go countdown(chWithMa)
go chat(who) //聊天机器人去聊天
//我现在正在玩游戏,我时刻听着母亲的呼喊
<-chWithMa //这里会阻塞一分钟,这一分钟是我的自由时间
close(chWithMa)
wg.Wait() //等待计数器为0,才终止程序
}
上述启动了两个goroutine,main函数如果不对自己执行流启动的goroutine进行统计,它盲目退出,有可能导致正在工作的goroutine被迫因为进程的退出的停止,为了让所有goroutine在完成工作之前,main函数都不退出,需要使用sync.WaitGroup来实现goroutine的同步。
无缓冲的通道又称为阻塞的通道。ch := make(chan int)
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。
package main
import (
"fmt"
)
func recv(c chan int) {
ret := <-c
fmt.Println("接收成功", ret)
}
func main() {
ch := make(chan int)
go recv(ch) // 启用goroutine从通道接收值
ch <- 10
fmt.Println("发送成功")
}
有缓冲通道ch := make(chan int,3)
指定了通道容量。
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。
从通道取值,还可以判断通道是否已经关闭,x := <- ch
可以接受两个值val,ok:=<-ch
第二个值是bool类型。
package main
import (
"fmt"
"strconv"
)
// channel 练习
func main(){
chat:=make(chan string)
go func(){
//往chat中写入数据
for i:=0;i<100;i++{
chat<-strconv.Itoa(i)
}
close(chat)
}()
for {
val,ok:=<-chat
if !ok{
break
}else{
fmt.Println(val)
}
}
}
实际上还可以通过for range方式去取值,而不用判断通道是否关闭。
for val:=range chat{
fmt.Println(val)
}
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。
package main
import (
"fmt"
"strconv"
)
func send(chat chan<- string){//send函数对于通道chat只写权限,不能从chat取值
//往chat中写入数据
for i:=0;i<100;i++{
chat<-strconv.Itoa(i)
}
close(chat)
}
func recv(chat <-chan string){//recv函数对于chat只读权限,不能向chat发送值
for val:=range chat{
fmt.Println(val)
}
}
func main(){
chat:=make(chan string)
go send(chat)
go recv(chat)
}
通道总结
channel常见的异常总结,如下图:
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。
select的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句。具体格式如下:
select{
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
//默认操作
}
package main
//作为新时代的好男人,既要听老婆的话也要孝顺听父母的
import (
"fmt"
"strconv"
"sync"
"time"
)
var wg sync.WaitGroup
func parent(mather chan<- string) {
defer wg.Done()
for i := 0; i < 10; i++ {
msg1 := "父母都是为你好"
msg2 := strconv.Itoa(i)
msg := fmt.Sprintf("%s%s", msg2, msg1)
mather <- msg
}
close(mather)
}
func woman(wife chan<- string) {
defer wg.Done()
for i := 0; i < 10; i++ {
msg1 := "老婆才是陪着你终老的人"
msg2 := strconv.Itoa(i)
msg := fmt.Sprintf("%s%s", msg2, msg1)
wife <- msg
}
close(wife)
}
func man(wife, mather <-chan string) {
defer wg.Done()
wifeOver := true
matherOver := true
for wifeOver || matherOver {
select {
case x, ok := <-wife:
if ok {
fmt.Println(x)
} else {
wifeOver = false
}
case y, ok := <-mather:
if ok {
fmt.Println(y)
} else {
matherOver = false
}
default:
fmt.Println("今天星期天")
}
time.Sleep(time.Second)
}
}
func main() {
wife := make(chan string)
mather := make(chan string, 3)
wg.Add(3)
go parent(mather)
go woman(wife)
go man(wife, mather)
wg.Wait()
}
使用select语句能提高代码的可读性。