在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言: 在我看来go是一门值得去学习去学习的语言。我本来是学习php的,有人会第一时间反驳我,php学习的咋样啊,就慌着去学习其他语言,我想说的是这不冲突,作为一个后端开发者,只会php一门脚本式的弱类型语言是远远不够的,这里不是说php语言不好。php有php的好,编译语言,强类型语言也自有他的优势所在,而服务器端开发者需要在并发,多线程上有所涉猎,总不能5年8年之后还写php吧,你要知道好多的架构师是没有语言的限制的。我就是一个不安分的人,不喜欢按部就班的生活,趁现在还年轻,喜欢啥就会全力去学习,好了,扯淡的话就说这么多。 这篇博客写的是go语言中的channel,之所以写他是因为我感觉channel很重要,同时channel也是go并发的重要支撑点,因为go是使用消息传递共享内存而不是使用共享内存来通信。并发编程是非常好的,但是并发是非常复杂的,难点在于协调,怎样处理各个程序间的通信是非常重要的。写channel的使用和特性之前我们需要回顾操作系统中的进程间的通信。 进程间的通信 在工程上一般通信模型有两种:共享数据和消息。进程通信顾名思义是指进程间的信息交换,因为进程的互斥和同步就需要进程间交换信息,学过操作系统的人都知道进程通信大致上可以分为低级进程通信和高级进程通信,现在基本上都是高级进程通信。其中高级通信机制又可以分为:消息传递系统、共享存储器系统、管道通信系统和客户机服务器系统。 channel的使用 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语言,没有做过系统的项目,只是比较感兴趣,希望以后深入学习这门语言,文章中不对之处或者是理解上的偏差请大神在评论处指出来,大家共同学习。 注意:
1、本博客同步更新到我的个人网站:http://www.zhaoyafei.cn
2、本文属原创内容,为了尊重他人劳动,转载请注明本文地址:
|
请发表评论