Go的并发之goroutine和channel篇
进程和线程相信大家已经很熟悉了,为了压榨多核处理器的性能,所有语言都大显神通,Go也不例外,但Go和其他语言又有些不同,需要用户自己控制并分配资源。
goroutine前言
这种机制在Go中成为goroutine,其实一直使用的main()函数就在使用它,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
尽管使不使用goroutine是我们控制的,但其本身运行时(runtime)的调度和管理还是由Go来控制的,Go程序会智能地将goroutine中的任务合理分配给每个CPU,我们也不需要操心进程调度和上下文切换的机制
goroutine用法
使用很简单,只需要使用关键字go来定义即可启动一个goroutine,我们创建一个例子试试
demo1.go(不使用goroutine)
package main
import (
"fmt"
"time"
)
func goroutineEmp(i int) {
fmt.Println("Hello, i m ", i)
time.Sleep(time.Second)
}
func main(){
startTime := time.Now()
for i := 0; i < 10; i++ {
goroutineEmp(i)
}
fmt.Println("============")
spendTime := time.Since(startTime)
fmt.Println("Spend Time:", spendTime)
}
输出结果
demo1.go(使用goroutine)
package main
import (
"fmt"
"time"
)
func goroutineEmp(i int) {
fmt.Println("Hello, i m ", i)
time.Sleep(time.Second)
fmt.Println("wake up!")
}
func main(){
startTime := time.Now()
for i := 0; i < 10; i++ {
go goroutineEmp(i)
}
fmt.Println("============")
spendTime := time.Since(startTime)
fmt.Println("Spend Time:", spendTime)
}
运行
可以发现goroutineEmp中wakeup还没启动,十个hello部分都完事儿了,是因为同时调用十个goroutine进程在一秒睡醒之前已经运行完事退出了,你多运行几次可能还会发现有的还没来得及打印hello就完事了,那么如何保证运行顺序,又用上让人又爱又恨的并发呢?下面我们引入进程管道的概念
Channel
关于channel有叫管道的,有叫信道的,这都无所谓,英文别错了就行,在并发执行函数中如果不能交换数据好像差点意思,而且这里交换数据可以解决我们的序列问题。你可能会问,不是可以用共享内存来进行数据交换吗,但那个在不同的goroutine中显然会导致数据不一致的问题,为了解决新的问题又要加锁,那还并发啥?
Go给出的方案是用通信的方法代替共享内存,也就是使用管道(Channel)
管道的数据结构是一个先进先出的队列,保证数据收发的顺序
创建方法
channel实例 := make(chan 数据类型)
chan1 := make(chan int) //创建一个整形的管道
发送&接收
ch := make(chan int) //声明一个int型的chan
ch <- 0 //管道发送消息(这里就是把0塞进去)
data := <-ch //<-ch取消息操作,将取出的消息赋给data
注意这里不能直接运行,只是举例,管道信息的收发需要在两个不同的goroutine中进行
我们来改改上面那个demo,引入管道的概念,来保证数据有序
demo2.go
package main
import (
"fmt"
"time"
)
func goroutineEmpChan(chan1 chan int) {
fmt.Println("Hello, i m from chan ", <-chan1)
time.Sleep(time.Second)
}
func main(){
startTime := time.Now()
chan1 := make(chan int)
for i := 0; i < 10; i++ {
go goroutineEmpChan(chan1)
chan1 <- i
}
fmt.Println("============")
spendTime := time.Since(startTime)
fmt.Println("Spend Time:", spendTime)
}
运行
发现符合预期,但管道(channel)还是有很多特性是不得不说的
我们发现上面的管道相当于拿到数据就丢出去了,并不做存储,如果一次来好几个数据没来得及被人取走怎么办呢?
无缓冲管道
demo3.go
func main(){
chan2 := make(chan int)
go func() {
for i := 0; i < 3; i++ {
chan2 <- i
fmt.Println("发送 ", i, " 给管道啦")
time.Sleep(time.Second)
}
}()
fmt.Println("收到的第一个数据是", <-chan2)
}
为了简单演示,我们只放一个匿名函数来测试,发现第二个数据已经发不出去了,这就好比生产者和消费者之间的关系,你生产了一个资源发给管道,这个管道一次只能收1个,而且如果这个不被消费掉,就不会收第二个,因为他不能存东西,没人消费他就不进货,离谱吧?这个时候我们就引入了有缓冲的管道
有缓冲管道
demo4.go
func main(){
chan3 := make(chan int, 1) //这里第二个参数就是channel的长度
go func() {
for i := 0; i < 3; i++ {
chan3 <- i
fmt.Println("发送 ", i, " 给管道啦")
}
}()
fmt.Println("收到的第一个数据是", <-chan3)
//fmt.Println("那有没有第二个数据呢?", <-chan3)
}
运行
可以发现我们取一次,也发了两条消息进去,是因为管道长度为1,可以额外装一个消息供消费,如果长度改成2呢?
确实 没毛病,如果你不相信呢,还可以调用len(channel)
来查询管道的长度哦