理解Go协程与并发

Go语言里创建一个协程很简单,使用go关键字就可以让一个普通方法协程化:

package main import ( "fmt" "time" ) func main(){ fmt.Println("run in main coroutine.") for i:=0; i<10; i++ { go func(i int) { fmt.Printf("run in child coroutine %d.\n", i) }(i) } //防止子协程还没有结束主协程就退出了 time.Sleep(time.Second * 1) }

下面这些概念可能不太好理解,需要慢慢理解。可以先跳过,回头再来看。

概念:

协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。

一个进程内部可以运行多个线程,而每个线程又可以运行很多协程。线程要负责对协程进行调度,保证每个协程都有机会得到执行。当一个协程睡眠时,它要将线程的运行权让给其它的协程来运行,而不能持续霸占这个线程。同一个线程内部最多只会有一个协程正在运行。

协程可以简化为三个状态:运行态、就绪态和休眠态。同一个线程中最多只会存在一个处于运行态的协程。就绪态协程是指那些具备了运行能力但是还没有得到运行机会的协程,它们随时会被调度到运行态;休眠态的协程还不具备运行能力,它们是在等待某些条件的发生,比如 IO 操作的完成、睡眠时间的结束等。

子协程的异常退出会将异常传播到主协程,直接会导致主协程也跟着挂掉。

协程一般用 TCP/HTTP/RPC服务、消息推送系统、聊天系统等。使用协程,我们可以很方便的搭建一个支持高并发的TCP或HTTP服务端。

通道

通道的英文是Channels,简称chan。什么时候要用到通道呢?可以先简单的理解为:协程在需要协作通信的时候就需要用通道。

在GO里,不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过通道。Go 语言鼓励使用通道的形式来交流。

举个简单的例子,我们使用协程实现并发调用远程接口,最终我们需要把每个协程请求回来的数据进行汇总一起返回,这个时候就用到通道了。

创建通道

创建通道(channel)只能使用make函数:

c := make(chan int)

通道是区分类型的,如这里的int。

Go 语言为通道的读写设计了特殊的箭头语法糖 <-,让我们使用通道时非常方便。把箭头写在通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。一次只能读写一个元素。

c := make(chan bool) c <- true //写入 <- c //读取 缓冲通道

上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素:

c := make(chan int, value)

当 value = 0 时,通道是无缓冲阻塞读写的,等价于make(chan int);当value > 0 时,通道有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。具体说明下:

非缓冲通道
无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。

缓冲通道
缓冲通道可以理解为消息队列,在有容量的时候,发送和接收是不会互相依赖的。用异步的方式传递数据。

下面我们用一个例子来理解一下:

package main import "fmt" func main() { var c = make(chan int, 0) var a string go func() { a = "hello world" <-c }() c <- 0 fmt.Println(a) }

这个例子输出的一定是hello world。但是如果你把通道的容量由0改为大于0的数字,输出结果就不一定是hello world了,很可能是空。为什么?

当通道是无缓冲通道时,执行到c <- 0,通道满了,写操作会被阻塞住,直到执行<-c解除阻塞,后面的语句接着执行。

要是改成非阻塞通道,执行到c <- 0,发现还能写入,主协程就不会阻塞了,但这时候输出的是空字符串还是hello world,取决于是子协程和主协程哪个运行的速度快。

通道作为容器,它可以像切片一样,使用 cap() 和 len() 全局函数获得通道的容量和当前内部的元素个数。

模拟消息队列

上一节"协程"的例子里,我们在主协程里加了个time.Sleep(),目的是防止子协程还没有结束主协程就退出了。但是对于实际生活的大多数场景来说,1秒是不够的,并且大部分时候我们都无法预知for循环内代码运行时间的长短。这时候就不能使用time.Sleep() 来完成等待操作了。下面我们用通道来改写:

package main import ( "fmt" ) func main() { fmt.Println("run in main coroutine.") count := 10 c := make(chan bool, count) for i := 0; i < count; i++ { go func(i int) { fmt.Printf("run in child coroutine %d.\n", i) c <- true }(i) } for i := 0; i < count; i++ { <-c } } 单向通道

默认的通道是支持读写的,我们可以定义单向通道:

//只读 var readOnlyChannel = make(<-chan int) //只写 var writeOnlyChannel = make(chan<- int)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/95a3a43bc0fa21cbdc737b160cb22deb.html