GO语言的进阶之路-goroutine(并发)html
做者:尹正杰程序员
版权声明:原创做品,谢绝转载!不然将追究法律责任。golang
有人把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