GO语言之channel

前言:
  初识go语言不到半年,我是一次偶然的机会认识了golang这门语言,看到他简洁的语法风格和强大的语言特性,瞬间有了学习他的兴趣。我是很看好go这样的语言的,一方面由于他有谷歌主推,另外一方面他确实有用武之地,高并发就是他的长处。如今的国内彻底使用go开发的项目还不是不少,从这个上面能够看到:连接https://github.com/qiniu/go/issues/15,据我了解七牛云存储应该是第一个彻底使用go开发的大型项目,其中七牛云的CEO许世伟是公认的go专家,同时也是《go语言编程》的做者,另外美团、小米、360、新浪等公司或多或少都有go语言的使用。php

  在我看来go是一门值得去学习去学习的语言。我原本是学习php的,有人会第一时间反驳我,php学习的咋样啊,就慌着去学习其余语言,我想说的是这不冲突,做为一个后端开发者,只会php一门脚本式的弱类型语言是远远不够的,这里不是说php语言很差。php有php的好,编译语言,强类型语言也自有他的优点所在,而服务器端开发者须要在并发,多线程上有所涉猎,总不能5年8年以后还写php吧,你要知道好多的架构师是没有语言的限制的。我就是一个不安分的人,不喜欢循序渐进的生活,趁如今还年轻,喜欢啥就会全力去学习,好了,扯淡的话就说这么多。html

  这篇博客写的是go语言中的channel,之因此写他是由于我感受channel很重要,同时channel也是go并发的重要支撑点,由于go是使用消息传递共享内存而不是使用共享内存来通讯。并发编程是很是好的,可是并发是很是复杂的,难点在于协调,怎样处理各个程序间的通讯是很是重要的。写channel的使用和特性以前咱们须要回顾操做系统中的进程间的通讯。git

进程间的通讯github

  在工程上通常通讯模型有两种:共享数据和消息。进程通讯顾名思义是指进程间的信息交换,由于进程的互斥和同步就须要进程间交换信息,学过操做系统的人都知道进程通讯大体上能够分为低级进程通讯和高级进程通讯,如今基本上都是高级进程通讯。其中高级通讯机制又能够分为:消息传递系统、共享存储器系统、管道通讯系统和客户机服务器系统。

  一、消息传递系统
  他不借助任何共享存储区或着某一种数据结构,他是以格式化的消息为单位利用系统提供的通讯原语完成数据交换,感受效率底下。

  二、共享存储器系统
  通讯的进程共享存储区或者数据结构,进程经过这些空间进行通讯,这种方式比较常见,好比某一个文件做为载体。

  三、客户机服务器系统
  其余几种通讯机制基本上都是在同一个计算机上(能够说是同一环境),固然在一些状况下能够实现跨计算机通讯。而客户机-服务器系统是不同的,个人理解是能够当作ip请求,一个客户机请求链接到一台服务器。这种方式在网络上是如今比较流行的,如今比较经常使用的远程调度,如不RPC(听着很高大上,其实在操做系统上早就有了)还有套接字、socket,这种仍是比较经常使用的,与咱们编程紧密相关的,由于你会发现好多的服务须要使用RPC调用。

  四、管道通讯系
  最后详细说一下管道通讯的机制,在操做系统级别管道是指用于连接一个读进程和一个写进程来实现他们之间通讯的文件。系统上叫pipe文件。实现的机制如:管道提供了下面的二个功能,一、互斥性,当一个进程正在对一个pipe文件执行读或者写操做时,其余的进程必须等待或阻塞或睡眠。二、同步性,当写(输入)进程写入pipe文件后会等待或者阻塞或者睡眠,直到读(输出)进程取走数据后把他唤醒,同理,当读进程去读一个空的pipe文件时也会等待或阻塞或睡眠,直到写进程写入pipe后把他唤醒。golang

channel的使用
  好了,上面花了很多的篇幅写了进程间通讯的几种方式,咱们再回过来看看channel,对应到go中的channel应该是第四种,go语言的channel是在语言级别提供的goroutine间通讯的方式。单独说channel是没有任何意义的,由于他和goroutine一块儿才有效果,咱们先看看通常语言解决程序间共享内存的方法,下面是一段咱们熟悉的程序,什么也不会输出,我刚学习的时候认为会输出东西,可是实际不是这样,当是感到一脸懵逼。编程

 1 package main
 2 
 3 import "fmt"
 4 
 5 var counts int = 0
 6 
 7 func Count() {
 8     counts++
 9     fmt.Println(counts)
10 }
11 func main() {
12 
13     for i := 0; i < 3; i++ {
14         go Count()
15     }
16 }

  学过go的人都应该知道缘由,由于:Go程序从初始化main() 方法和package,而后执行main()函数,可是当main()函数返回时,程序就会退出,主程序并不等待其余goroutine的,致使没有任何输出。咱们看看常规语言是怎样解决这种并发的问题的:后端

 1 package main
 2 
 3 import "fmt"
 4 import "sync"
 5 import "runtime"
 6 
 7 var counts int = 0
 8 
 9 func Count(lock *sync.Mutex) {
10     lock.Lock()
11     counts++
12     fmt.Println(counts)
13     lock.Unlock()
14 }
15 func main() {
16     lock := &sync.Mutex{}
17 
18     for i := 0; i < 3; i++ {
19         go Count(lock)
20     }
21 
22     for {
23         lock.Lock()
24         c := counts
25         lock.Unlock()
26 
27         runtime.Gosched()
28 
29         if c >= 3 {
30             break
31         }
32 
33     }
34 }

  解决方式有点逗比,加了一堆的锁,由于他的执行是这样的:代码中的lock变量,每次对counts的操做,都要先将他锁住,操做完成后,再将锁打开,在主函数中,使用for循环来不断检查counter的值固然一样也要加锁。当其值达到3时,说明全部goroutine都执行完毕了,这时主函数返回,而后程序退出。这种方式是大众语言解决并发的首选方式,能够看到为了解决并发,多写了好多的东西,若是一个初具规模的项目,不知道要加多少锁。七牛云存储

  咱们看看channel是如何解决这种问题的:服务器

 1 package main
 2 
 3 import "fmt"
 4 
 5 var counts int = 0
 6 
 7 func Count(i int, ch chan int) {
 8     fmt.Println(i, "WriteStart")
 9     ch <- 1
10     fmt.Println(i, "WriteEnd")
11     fmt.Println(i, "end", "and echo", i)
12     counts++
13 }
14 
15 func main() {
16     chs := make([]chan int, 3)
17     for i := 0; i < 3; i++ {
18         chs[i] = make(chan int)
19         fmt.Println(i, "ForStart")
20         go Count(i, chs[i])
21         fmt.Println(i, "ForEnd")
22     }
23 
24     fmt.Println("Start debug")
25     for num, ch := range chs {
26         fmt.Println(num, "ReadStart")
27         <-ch
28         fmt.Println(num, "ReadEnd")
29     }
30 
31     fmt.Println("End")
32 
33     //为了使每一步数值所有打印
34     for {
35         if counts == 3 {
36             break
37         }
38     }
39 }

为了看清goroutine执行的步骤和channel的特性,我特地在每一步都作了打印,下面是执行的结果,感兴趣的同窗能够本身试试,打印的顺序可能不同:网络

  下面咱们分析一下这个流程,看看channel在里面的做用。主程序开始:

  打印 "0 ForStart 0 ForEnd" ,表示 i = 0 这个循环已经开始执行了,第一个goroutine已经开始;

  打印 "1 ForStart"、"1 ForEnd"、"2 ForStart"、"2 ForEnd" 说明3次循环都开始,如今系统中存在3个goroutine;

  打印 "Start debug",说明主程序继续往下走了,

  打印 "0 ReadStar"t ,说明主程序执行到for循环,开始遍历chs,一开始遍历第一个,可是由于此时 i = 0 的channel为空,因此该channel的Read操做阻塞;

  打印 "2 WriteStart",说明第一个 i = 2 的goroutine先执行到Count方法,准备写入channel,由于主程序读取 i = 0 的channel的操做再阻塞中,因此 i = 2的channel的读取操做没有执行,如今i = 2 的goroutine 写入channel后下面的操做阻塞;

  打印 "0 WriteEnd",说明 i = 0 的goroutine也执行到Count方法,准备写入channel,此时主程序 i = 0 的channel的读取操做被唤醒;

  打印 "0 WriteEnd" 和 "0 end and echo 0" 说明写入成功;

  打印 "0 ReadEnd",说明唤醒的 i = 0 的channel的读取操做已经唤醒,而且读取了这个channel的数据;

  打印 "0 ReadEnd",说明这个读取操做结束;

  打印 "1 ReadStart",说明 i = 1 的channel读取操做开始,由于i = 1 的channel没有内容,这个读取操做只能阻塞;

  打印 "1 WriteStart",说明 i = 1 的goroutine 执行到Count方法,开始写入channel 此时 i = 1的channel读取操做被唤醒;

  打印 "1 WriteEnd" 和 "1 end and echo 1" 说明 i = 1 的channel写入操做完成;

  打印 "1 ReadEnd",说明 i = 1 的读取操做完成;

  打印 "2 ReadStart",说明 i = 2 的channel的读取操做开始,由于以前已经执行到 i = 2 的goroutine写入channel操做,只是阻塞了,如今由于读取操做的进行,i = 2的写入操做流程继续执行;

  打印 "2 ReadEnd",说明 i = 2 的channel读取操做完成;

  打印 "End" 说明主程序结束。

  此时可能你会有疑问,i = 2 的goroutine尚未结束,主程序为啥就结束了,这正好印证了咱们开始的时候说的,主程序是不等待非主程序完成的,因此按照正常的流程咱们看不到 i = 2 的goroutine的的彻底结束,这里为了看到他的结束我特地加了一个 counts 计算器,只有等到计算器等于3的时候才结束主程序,接着就出现了打印 "2 WriteEnd" 和 "2 end and echo 2"  到此全部的程序结束,这就是goroutine在channel做用下的执行流程。

  上面分析写的的比较详细,耐心看两遍基本上就明白了,主要帮助你们理解channel的写入阻塞和读入阻塞的应用。

 

基本语法

channel的基本语法比较简单, 通常的声明格式是:

1 var ch chan ElementType

定义格式以下:

1 ch := make(chan int)

还有一个最经常使用的就是写入和读出,当你向channel写入数据时会致使程序阻塞,直到有其余goroutine从这个channel中读取数据,同理若是channel以前没有写入过数据,那么从channel中读取数据也会致使程序阻塞,直到这个channel中被写入了数据为止

1 ch <- value    //写入
2 value := <-ch  //读取

关闭channel

close(ch)

判断channel是否关闭(利用多返回值的方式):

1 b, status := <-ch

带缓冲的channel,提及来也容易,以前咱们使用的都是不带缓冲的channel,这种方法适用于单个数据的状况,对于大量的数据不太实用,在调用make()的时候将缓冲区大小做为第二个参数传入就能够建立缓冲的channel,即便没有读取方,写入方也能够一直往channel里写入,在缓冲区被填完以前都不会阻塞。

c := make(chan int, 1024)

单项channel,单向channel只能用于写入或者读取数据。channel自己必然是同时支持读写的,不然根本无法用。所谓的单向channel概念,其实只是对channel的一种使用限制。单向channel变量的声明:

1 var ch1 chan int   // ch1是一个正常的channel
2 var ch2 <-chan int // ch2是单向channel,只用于读取int数据

单项channel的初始化

1 ch3 := make(chan int)
2 ch4 := <-chan int(ch3) // ch4是一个单向的读取channel

 

超时机制

  超时机制其实也是channel的错误处理,channel当然好用,可是有时不免会出现实用错误,当是读取channel的时候发现channel为空,若是没有错误处理,像这种状况就会使整个goroutine锁死了,没法运行,我找了好多资料和说法,channel 并无处理超时的方法,可是能够利用其它方法间接的处理这个问题,可使用select机制处理,select的特色比较明显,只要有一个case完成了程序就会往下运行,利用这种方法,能够实现channel的超时处理:

  原理以下:咱们能够先定义一个channel,在一个方法中对这个channel进行写入操做,可是这个写入操做比较特殊,好比咱们控制5s以后写入到这个channel中,这5s时间就是其余channel的超时时间,这样的话5s之后若是还有channel在执行,能够判断为超时,这是channel写入了内容,select检测到有内容就会执行这个case,而后程序就会顺利往下走了。实现以下:

 1 timeout := make(chan bool, 1)
 2 go func() {
 3     time.Sleep(5s) // 等待s秒钟
 4     timeout <- true
 5 }()
 6 
 7 select {
 8     case <-ch:
 9     // 从ch中读取到数据
10     case <-timeout:
11     // 没有从ch中读取到数据,但从timeout中读取到了数据
12 }

好了,今天就写这么多,写了一上午了,该吃饭了。

初学go语言,没有作过系统的项目,只是比较感兴趣,但愿之后深刻学习这门语言,文章中不对之处或者是理解上的误差请大神在评论处指出来,你们共同窗习。

注意:
一、本博客同步更新到个人我的网站:http://www.zhaoyafei.cn
二、本文属原创内容,为了尊重他人劳动,转载请注明本文地址:
相关文章
相关标签/搜索