GO语言的进阶之路-goroutine(并发)
GO语言的进阶之路-goroutine(并发)
作者:尹正杰
版权声明:原创作品,谢绝转载!否则将追究法律责任。
有人把Go比作21世纪的C 语言,第一是因为 Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而GO 从语言层面就支持了并行。Go语言中最重要的一个特性,那就是 go 关键字。优雅的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。使用Go语言开发服务器程序时,就需要对它的并发机制有深入的了解。
一.并发基础
回到在Windows和Linux出现之前的古老年代,程序员在开发程序时并没有并发的概念,因为命令式程序设计语言是以串行为基础的,程序会顺序执行每一条指令,整个程序只有一个执行上下文,即一个调用栈,一个堆。并发则意味着程序在运行时有多个执行上下文,对应着多个调用栈。我们知道每一个进程在运行时,都有自己的调用栈和堆,有一个完整的上下文,而操作系统在调度进程的时候,会保存被调度进程的上下文环境,等该进程获得时间片后,再恢复该进程的上下文到系统中。从整个操作系统层面来说,多个进程是可以并发的,那么并发的价值何在?下面我们先看以下几种场景。
1>.一方面我们需要灵敏响应的图形用户界面,一方面程序还需要执行大量的运算或者IO密集操作,而我们需要让界面响应与运算同时执行。
2>.当我们的Web服务器面对大量用户请求时,需要有更多的“Web服务器工作单元”来分别响应用户。
3>.我们的事务处于分布式环境上,相同的工作单元在不同的计算机上处理着被分片的数据。
4>.计算机的CPU从单内核(core)向多内核发展,而我们的程序都是串行的,计算机硬件的能力没有得到发挥。
5>.我们的程序因为IO操作被阻塞,整个程序处于停滞状态,其他IO无关的任务无法执行。
从以上几个例子可以看到,串行程序在很多场景下无法满足我们的要求。下面我们归纳了并发程序的几条优点,让大家认识到并发势在必行:
a>.并发能更客观地表现问题模型;
b>.并发可以充分利用CPU核心的优势,提高程序的执行效率;
c>.并发能充分利用CPU与其他硬件设备固有的异步性。
现在我们已经意识到并发的好处了,那么到底有哪些方式可以实现并发执行呢?就目前而言,并发包含以下几种主流的实现模型。
1>.多进程。多进程是在操作系统层面进行并发的基本模式。同时也是开销最大的模式。在Linux平台上,很多工具链正是采用这种模式在工作。比如某个Web服务器,它会有专门的进程负责网络端口的监听和链接管理,还会有专门的进程负责事务和运算。这种方法的好处在于简单、进程间互不影响,坏处在于系统开销大,因为所有的进程都是由内核管理的。
2>.多线程。多线程在大部分操作系统上都属于系统层面的并发模式,也是我们使用最多的最有效的一种模式。目前,我们所见的几乎所有工具链都会使用这种模式。它比多进程 的开销小很多,但是其开销依旧比较大,且在高并发模式下,效率会有影响。
3>.基于回调的非阻塞/异步IO。这种架构的诞生实际上来源于多线程模式的危机。在很多高并发服务器开发实践中,使用多线程模式会很快耗尽服务器的内存和CPU资源。而这种模式通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复杂,因为它把流程做了分割,对于问题本身的反应不够自然。
4>.协程。协程(Coroutine)本质上是一种用户态线程,不需要操作系统来进行抢占式调度,且在真正的实现中寄存于线程中,因此,系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。使用协程的优点是编程简单,结构清晰;缺点是需要语言的支持,如果不支持,则需要用户在程序中自行实现调度器。目前,原生支持协程的语言还很少。
接下来我们先诠释一下传统并发模型的缺陷,之后再讲解goroutine并发模型是如何逐一解决这些缺陷的。
人的思维模式可以认为是串行的,而且串行的事务具有确定性。线程类并发模式在原先的确定性中引入了不确定性,这种不确定性给程序的行为带来了意外和危害,也让程序变得不可控。线程之间通信只能采用共享内存的方式。为了保证共享内存的有效性,我们采取了很多措施,比如加锁等,来避免死锁或资源竞争。实践证明,我们很难面面俱到,往往会在工程中遇到各种奇怪的故障和问题。
我们可以将之前的线程加共享内存的方式归纳为“共享内存系统”,虽然共享内存系统是一种有效的并发模式,但它也暴露了众多使用上的问题。计算机科学家们在近40年的研究中又产生了一种新的系统模型,称为“消息传递系统”。
对线程间共享状态的各种操作都被封装在线程之间传递的消息中,这通常要求:发送消息时对状态进行复制,并且在消息传递的边界上交出这个状态的所有权。从逻辑上来看,这个操作与共享内存系统中执行的原子更新操作相同,但从物理上来看则非常不同。由于需要执行复制操作,所以大多数消息传递的实现在性能上并不优越,但线程中的状态管理工作通常会变得更为简单。
最早被广泛应用的消息传递系统是由C. A. R. Hoare在他的Communicating Sequential Processes中提出的。在CSP系统中,所有的并发操作都是通过独立线程以异步运行的方式来实现的。这些线程必须通过在彼此之间发送消息,从而向另一个线程请求信息或者将信息提供给另一个线程。使用类似CSP的系统将提高编程的抽象级别。
随着时间的推移,一些语言开始完善消息传递系统,并以此为核心支持并发,比如Erlang。
二.协程
再说协成之前,我们需要了解两个概念,即用户态和内核态。
1.什么是用户态;
官方解释:用户态(user mode)在计算机结构指两项类似的概念。在CPU的设计中,用户态指非特权状态。在此状态下,执行的代码被硬件限定,不能进行某些操作,比如写入其他进程的存储空间,以防止给操作系统带来安全隐患。在操作系统的设计中,用户态也类似,指非特权的执行状态。内核禁止此状态下的代码进行潜在危险的操作,比如写入系统配置文件、杀掉其他用户的进程、重启系统等。
应用程序在用户态下运行,仅仅只能执行cpu整个指令集的一个子集,该子集中不包含操作硬件功能的部分,因此,一般情况下,在用户态中有关I/O和内存保护(操作系统占用的内存是受保护的,不能被别的程序占用)。
如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E7%94%A8%E6%88%B7%E6%80%81/9548791?fr=aladdin
2.什么是内核态;
内核态也叫和核心态。
官方解释:在处理器的存储保护中,主要有两种权限状态,一种是核心态(管态),也被称为特权态;一种是用户态(目态)。核心态是操作系统内核所运行的模式,运行在该模式的代码,可以无限制地对系统存储、外部设备进行访问。
操作系统在内核态运行情况下可以访问硬件上所有的内容。
如果感兴趣的朋友可以参考:https://baike.baidu.com/item/%E6%A0%B8%E5%BF%83%E6%80%81/6845908?fr=aladdin
3.什么是协程;
官方解释:一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
执行体是个抽象的概念,在操作系统层面有多个概念与之对应,比如操作系统自己掌管的进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)。与传统的系统级线程和进程相比,协程的最大优势在于其“轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万个。这也是协程也叫轻量级线程的原因。
多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供轻量级线程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。
Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的所有系统调用操作(当然也包括所有同步 IO 操作),都会出让 CPU 给其他goroutine。这让事情变得非常简单,让轻量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。协程(coroutine)是Go语言中的轻量级线程实现,由Go运行时(runtime)管理。在一个函数调用前加上go关键字,这次调用就会在一个新的goroutine中并发执行。当被调用的函数返回时,这个goroutine也自动结束。需要注意的是,如果这个函数有返回值,那么这个返回值会被丢弃。协成工作在用户态,它类似于现场的运行方式可以并行处理任务。
三.goroutine
goroutine不同于thread,threads是操作系统中的对于一个独立运行实例的描述,不同操作系统,对于thread的实现也不尽相同;但是,操作系统并不知道goroutine的存在,goroutine的调度是有Golang运行时进行管理的。启动thread虽然比process所需的资源要少,但是多个thread之间的上下文切换仍然是需要大量的工作的(寄存器/Program Count/Stack Pointer/...),Golang有自己的调度器,许多goroutine的数据都是共享的,因此goroutine之间的切换会快很多,启动goroutine所耗费的资源也很少,一个Golang程序同时存在几百个goroutine是很正常的。goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理.goroutine 比thread 更易用、更高效、更轻便。
1.创建一个goroutine
goroutine 是通过 Go 的 runtime管理的一个线程管理器。通过关键字go 就启动了一个 goroutine。我们来看一个例子:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func MyEcho(s string) { 16 for i := 0; i < 5; i++ { 17 time.Sleep(100*time.Millisecond) //表示每次循环后都要休息100毫秒。 18 fmt.Println(s) 19 } 20 } 21 22 func main() { 23 go MyEcho("尹正杰") //在函数执行前加个go,表示单独起了一个协程,表示和当前主协程(main)并驾齐驱运行代码。 24 MyEcho("Golang") 25 } 26 27 28 29 30 31 #以上代码执行结果如下:(需要注意的是,他们输出的顺序是不确定的哟~) 32 Golang 33 尹正杰 34 Golang 35 尹正杰 36 尹正杰 37 Golang 38 Golang 39 尹正杰 40 尹正杰 41 Golang
2.goroutine的局限性
Go程序从初始化 main package 并执行 main() 函数开始,当 main() 函数返回时,程序退出,且程序并不等待其他goroutine(非主goroutine)结束。光这样说大家可能不是很理解,接下来我们就用实际代码来说明,下面的一段代码使对切片“yzj”的元素进行排序,而主程序运行时间是12秒,我们可以清楚的看到“yzj”这个切片的长度是13,这意味着需要开启13个goroutine,而我们定义的主函数的运行的时间是12秒。这意味着12秒之后,不管有多少个goroutine在运行程序都会自动结束。这也是为什么我们没有看到“yzj”这个切片数字的另外两个元素输出,即都是int型的15和17。因为这2个goroutine执行完毕的时间是15秒和17秒,而程序的最长允许的运行时间是12秒。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 yzj := []int{2,7,1,6,4,3,11,15,17,5,8,9,12} 17 fmt.Println("该切片的长度是:",len(yzj)) 18 for _,n := range yzj{ 19 go func(n int) { //定义一个匿名函数,并对该函数开启协程,每次循环都会开启一个协成,也就是说它开启了13个协程。 20 time.Sleep(time.Duration(n) * time.Second) //表示每循环一次就需要睡1s,睡的总时间是由n来控制的,总长度是由s切片数组中最大的一个数字决定,也就是说这个协成最少需要17秒才会结束哟。 21 fmt.Println(n) 22 }(n) //由于这个函数是匿名函数,所以调用方式就直接:(n)调用,不用输入函数名。 23 } 24 time.Sleep(12*time.Second) //主进程要执行的时间是12秒. 25 } 26 27 28 29 #以上代码执行结果如下: 30 该切片的长度是: 13 31 1 32 2 33 3 34 4 35 5 36 6 37 7 38 8 39 9 40 11 41 12
通过上面这段代码我们可以明显的知道这个程序是有bug的,因为我们的要求是对切片“yzj”顺序的从小到大的排序。但是“yzj”这个切片中的元素“15”和“17”是没有输出出来的。当然从上面的分析你会立马找出解决方案,比如说将主程序的运行时间从12秒改成大于或等于17秒不就得了。good,这种改法的确是可以针对这个程序是有效的。但是你没有发现这个效率很低吗?那么我们是不是有一种机制可以让goroutine和main()进行通信呢?要让主函数等待所有goroutine退出后再返回,如何知道goroutine都退出了呢?这就引出了多个goroutine之间通信的问题。
实现一个如此简单的功能,却写出如此臃肿而且难以理解的代码。想象一下,在一个大的系统中具有无数的锁、无数的共享变量、无数的业务逻辑与错误处理分支,那将是一场噩梦。这噩梦就是众多C/C++开发者正在经历的,其实Java和C#开发者也好不到哪里去。
Go语言既然以并发编程作为语言的最核心优势,当然不至于将这样的问题用这么无奈的方式来解决。Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式。消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。
Go语言提供的消息通信机制被称为channel,接下来我们将详细介绍channel。现在,让我们用Go语言社区的那句著名的口号来结束这一小节:“不要通过共享内存来通信,而应该通过通信来共享内存。”不过想要了解golang关于锁的通信机制的小伙伴们,我也将笔记早就总结出来了(使劲戳我就成)。
四.channel
channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用分布式系统的方法来解决,比如使用Socket或者HTTP等通信协议。Go语言对于网络方面也有非常完善的支持。
channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。如果对Unix管道有所了解的话,就不难理解channel,可以将其认为是一种类型安全的管道。
在了解channel的语法前,我们先看下用channel的方式重写上面的例子是什么样子的,以此对channel先有一个直感的认识。
需要重新上面案例。暂时空出来,等我把channel讲解完毕在写
1.基本语法(channels)
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func main() { 13 var yzj_string chan string //般channel的声明形式为:var chanName chan ElementType.与一般的变量声明不同的地方仅仅是在类型之前加了 chan 关键字。 ElementType 指定这个 channel所能传递的元素类型。 14 15 var yzj_map map[string]chan bool //这是我们声明一个的map ,元素是 bool 型的channel。 16 17 yzj_channel := make(chan []map[string]int)//定义一个channel也很简单,直接使用内置的函数 make() 即可。 18 19 fmt.Println(yzj_string) 20 fmt.Println(yzj_map) 21 fmt.Println(yzj_channel) 22 /* 23 writ_channel := "yinzhengjie" 24 25 yzj_string <- writ_channel //在channel的用法中,最常见的包括写入和读出。将一个数据写入(发送)至channel的语法很直观。向channel写入数据通常会导致程序阻塞,直到有其他goroutine从这个channel中读取数据。 26 27 read_channel := yzj_string //如果channel之前没有写入数据,那么从channel中读取数据也会导致程序阻塞,直到channel中被写入数据为止。我们之后还会提到如何控制channel只接受写或者只允许读取,即单向channel。 28 */ 29 } 30 31 32 33 #以上代码执行结果如下: 34 <nil> 35 map[] 36 0xc04203a060
知道如何定义一个channel之后,我们也可以做一下简单的应用,代码如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func MySum(a []int, sum chan int) { //该函数是对切片数组求和,需要传入一个切片数组和一个channel。 13 value := 0 14 for _, v := range a { 15 value += v 16 } 17 sum <- value //将数据发送到channel中去。 18 } 19 func main() { 20 yzj := []int{1,2,3,4,5,-10} 21 sum := make(chan int) //用chan定义一个channel对象名称为“sum”,其类型是“int”。 22 go MySum(yzj[:len(yzj)/2], sum) //将切片的前一半发送给channel对象“sum” 23 go MySum(yzj[len(yzj)/2:], sum) //将切片的后一半发送给channel对象“sum” 24 x, y := <-sum, <-sum //从我们定义中的channel中获取数据,并将读取到的value赋值给x,y 25 fmt.Println("X =" ,x) 26 fmt.Println("Y =",y) 27 fmt.Println("X+Y =" ,x+y) 28 } 29 30 31 32 #以上代码执行结果如下: 33 X = 6 34 Y = -1 35 X+Y = 5
2.缓冲机制(Buffered Channels)
之前我们示范创建的都是不带缓冲的channel,这种做法对于传递单个数据的场景可以接受,但对于需要持续传输大量数据的场景就有些不合适了。接下来我们介绍如何给channel带上缓冲,从而达到消息队列的效果。要创建一个带缓冲的channel,其实也非常容易,比如“yzj := make(chan int ,4096)”在调用 make() 时将缓冲区大小作为第二个参数传入即可,创建了一个大小为4096的 int 类型 channel ,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 import ( 10 "fmt" 11 ) 12 13 func FibonacciSequence(num int, Producer chan int) { 14 x, y := 1, 1 15 for i := 0; i < num; i++ { 16 Producer <- x 17 x, y = y, x + y 18 } 19 close(Producer) //可以显式的关闭channel,生产者通过关键字 close 函数关闭 channel。关闭channel 之后就无法再接受或发送任何数据了。 记住应该在生产者的地方关闭channel, 20 // 而不是消费的地方去关闭它,这样容易引起 panic 另外记住一点的就是channel 不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束 range 循环之类的。 21 22 } 23 24 func main() { 25 yzj := make(chan int, 5) 26 go FibonacciSequence(cap(yzj), yzj) 27 28 value, status := <-yzj //注意,这里的“value”相当对“yzj”这个channel进行读取一次数据哟。“status”的值如何为“true”则表明channel还没有被关闭哟。 29 fmt.Println(value,status) 30 31 for i := range yzj { //我们使用range语法能够不断的读取channel 里面的数据,直到该 channel 被显式的关闭。 32 fmt.Println(i) 33 } 34 35 value, status = <-yzj //注意,“status”的值如何为“false”,那么说明 channel 已经没有任何数据并且已经被关闭。 36 fmt.Println(value,status) 37 } 38 39 40 41 42 #以上代码执行结果如下: 43 1 true 44 1 45 2 46 3 47 5 48 0 false
3.channel的选择语句selecte语法
早在Unix时代, select 机制就已经被引入。通过调用 select() 函数来监控一系列的文件句柄,一旦其中一个文件句柄发生了IO动作,该 select() 调用就会被返回。后来该机制也被用于实现高并发的Socket服务器程序。Go语言直接在语言级别支持 select 关键字,用于处理异步IO问题。select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由case 语句来描述。与 switch 语句可以选择任何可使用相等比较的条件相比, select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作。
我们上面介绍的都是只有一个channel 的情况,那么如果存在多个 channel 的时候,我们该如何操作呢,Go里面提供了一个关键字 select ,通过 select 可以监听channel 上的数据流动。select 默认是阻塞的,只有当监听的channel 中有发送或接收可以进行时才会运行,当多个channel 都准备好的时候,select 是随机的选择一个执行的。
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func fibonacci(channel_name,quit chan int) { //定义两个channle对象channel_name和quit。 13 x,y := 0,1 14 for{ 15 select { 16 case channel_name <- x: //用channel_name接受数据。 17 x,y = y,x+y 18 19 case <-quit: //表示当接收到quit的channel时,就执行以下代码。其实就是实现关闭channel的功能。但是它并没有权限主动关闭channel,而是负责监听channel 上的数据流动。 20 fmt.Println("EXIT") 21 return //函数一退出协程也就跟着退出了 22 } 23 } 24 } 25 26 func main() { 27 channel_name := make(chan int) 28 quit := make(chan int) 29 30 go func() { //运行一个匿名函数。 31 for i := 0; i < 11; i++ { 32 fmt.Println(<-channel_name) //"<-channel_name"表示读取channel_name中的参数。 33 } 34 quit<- 100 //当for循环结束后,我们随便给quit的channel传一个值就可以实现退出函数的功能,我们之前需要用close(c)来退出发信号的功能,主动权在"fibonacci",而我们现在我们用quit来主动退出协程。 35 }() 36 37 fibonacci(channel_name,quit) //将channel_name和quit传递给fibonacci函数 38 } 39 40 41 42 43 #以上代码执行结果如下: 44 0 45 1 46 1 47 2 48 3 49 5 50 8 51 13 52 21 53 34 54 55 55 EXIT
4.channel的默认语句default语法
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 tick := time.Tick(1000*time.Millisecond) //也可以这样写:“tick := time.NewTicker(1000*time.Millisecond).C”其中这个点C就是一个channel。 17 boom := time.After(5000*time.Millisecond) 18 for { 19 select { 20 case <-tick: 21 fmt.Println("滴答。。。") 22 case <-boom: 23 fmt.Println("砰~") 24 return 25 default: 26 fmt.Println("吃一口凉皮") 27 time.Sleep(500*time.Millisecond) 28 } 29 } 30 } 31 32 33 34 35 #以上代码执行结果如下: 36 吃一口凉皮 37 吃一口凉皮 38 滴答。。。 39 吃一口凉皮 40 吃一口凉皮 41 滴答。。。 42 吃一口凉皮 43 吃一口凉皮 44 滴答。。。 45 吃一口凉皮 46 吃一口凉皮 47 滴答。。。 48 吃一口凉皮 49 吃一口凉皮 50 滴答。。。 51 砰~
5.超时机制(timeout)
Go语言没有提供直接的超时处理机制,但我们可以利用 select 机制。虽然 select 机制不是专为超时而设计的,却能很方便地解决超时问题。因为 select 的特点是只要其中一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。有时候会出现goroutine 阻塞的情况,那么我们如何避免整个的程序进入阻塞的情况呢?我们可以利用select 来设置超时,通过如下的方式实现:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "time" 12 "fmt" 13 ) 14 15 func main() { 16 TimeOut := time. After(5 * time.Second) //定义超时时间。 17 NerverRings := make(chan int) 18 RingsOccasionally := make(chan bool) 19 go func() { 20 for { 21 select { 22 case value := <- NerverRings: 23 fmt.Println(value) 24 case <- TimeOut: 25 println("对不起,到目前为止,NerverRings并没有接收到任何数据!程序已经终止。") 26 RingsOccasionally <- true 27 break 28 } 29 } 30 }() 31 32 <- RingsOccasionally //从RingsOccasionally这个channel中获取数据,所以在获取数据之前,成功程序是出于阻塞状态的哟。 33 34 } 35 36 37 38 #以上代码执行结果如下: 39 对不起,到目前为止,NerverRings并没有接收到任何数据!程序已经终止。
6.channel的传递
需要注意的是,在Go语言中channel本身也是一个原生类型,与 map 之类的类型地位一样,因此channel本身在定义后也可以通过channel来传递。具体案例如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "fmt" 12 "time" 13 ) 14 15 type PipeData struct { //定义结构体 PipeData 16 value int 17 handler func(int) int 18 next chan int 19 } 20 21 func Handle(queue chan *PipeData) { 22 for data := range queue{ 23 fmt.Println("value=",data.value) 24 fmt.Println("handler=",data.handler) 25 fmt.Println("next=",data.next) 26 data.next <- data.handler(data.value) 27 } 28 } 29 30 func main() { 31 yzj := make(chan *PipeData) //由于Handle支持传入指针类型的*PipeData,因此我们初始化的时候要个其类型保持一致。 32 33 go func() { //我们开启一个goroutine,让其不断的发送数据。 34 data := &PipeData{value:100,handler: func(i int) int { 35 return i 36 }} //我们需要将数据定义好,这个data就是我们需要发送的数据。 37 yzj <- data //将数据发送给名为yzj的channel。 38 }() 39 40 go Handle(yzj) //当我们把数据传给channel变量yzj之后,就可以把这个channel继续传给Handle这个函数啦。 41 // 42 //data := <- yzj //接下来我们开始从channel读取数据。 43 //fmt.Println(data.value) 44 //fmt.Println(data.handler) 45 //fmt.Println(data.next) 46 time.Sleep(time.Second * 3) //为了避免主进程提前结束,我们需要让主进程拖长一点时间,以后我会介绍更简单的方法来控制这个时间。 47 } 48 49 50 51 52 #以上代码执行结果如下: 53 value= 100 54 handler= 0x489250 55 next= <nil>
7.单向channel
顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。
定义一个单向channel很简单:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import "fmt" 11 12 func main() { 13 data := make(chan int) //默认该channel就是可读可写的哟。 14 15 go func() { 16 write := chan<- int(data) //此处的write 是一个单向的写入channel。 17 write <- 100 18 fmt.Println(write) 19 }() 20 21 read := <-chan int(data) //此处的read就是一个单向的读取channel。 22 fmt.Println(read) 23 } 24 25 26 27 #以上代码执行结果如下: 28 0xc04203a060 29 100
当然,我们可以在函数中对channel进行只读或是只写的操作,如下:
1 /* 2 #!/usr/bin/env gorun 3 @author :yinzhengjie 4 Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/ 5 EMAIL:y1053419035@qq.com 6 */ 7 8 package main 9 10 import ( 11 "fmt" 12 "time" 13 ) 14 15 func Parse(ch <-chan int) { //Parse函数的功能是只读的channel。注意的是,channel本身就是可读可写的,所谓的只读channel和只写channel只是使用者在用法上的限制而已。 16 for value := range ch { 17 fmt.Println("Parsing value", value) 18 } 19 } 20 21 func main() { 22 data := make(chan int) 23 go Parse(data) //注意,这行代码是阻塞代码。我们知道这行代码使从channel中读取数据,但是目前还没往channel发送数据。我们用go关键字开启一个协程,可以让代码继续往下执行。 24 data <- 10000 //往channel发送的数据。 25 time.Sleep(time.Second * 1) //让主程序运行一秒钟,避免主进程提前比goroutine结束。 26 27 } 28 29 30 31 #以上代码执行结果如下: 32 Parsing value 10000