教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 GO语言之channel

GO语言之channel

发布时间:2021-12-31   编辑:jiaochengji.com
教程集为您提供GO语言之channel等资源,欢迎您收藏本站,我们将为您提供最新的GO语言之channel资源
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"/></svg><h1>前言:</h1>

初识go语言不到半年,我是一次偶然的机会认识了golang这门语言,看到他简洁的语法风格和强大的语言特性,瞬间有了学习他的兴趣。我是很看好go这样的语言的,一方面因为他有谷歌主推,另一方面他确实有用武之地,高并发就是他的长处。现在的国内完全使用go开发的项目还不是很多,从这个上面可以看到:链接https://github.com/qiniu/go/issues/15,据我了解七牛云存储应该是第一个完全使用go开发的大型项目,其中七牛云的CEO许世伟是公认的go专家,同时也是《go语言编程》的作者,另外美团、小米、360、新浪等公司或多或少都有go语言的使用。
  这篇博客写的是go语言中的channel,之所以写他是因为我感觉channel很重要,同时channel也是go并发的重要支撑点,因为go是使用消息传递共享内存而不是使用共享内存来通信。并发编程是非常好的,但是并发是非常复杂的,难点在于协调,怎样处理各个程序间的通信是非常重要的。写channel的使用和特性之前我们需要回顾操作系统中的进程间的通信。

<h1>
进程间的通信</h1>

在工程上一般通信模型有两种:共享数据和消息。进程通信顾名思义是指进程间的信息交换,因为进程的互斥和同步就需要进程间交换信息,学过操作系统的人都知道进程通信大致上可以分为低级进程通信和高级进程通信,现在基本上都是高级进程通信。其中高级通信机制又可以分为:消息传递系统、共享存储器系统、管道通信系统和客户机服务器系统。
  1、消息传递系统
  他不借助任何共享存储区或着某一种数据结构,他是以格式化的消息为单位利用系统提供的通信原语完成数据交换,感觉效率底下。

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

3、客户机服务器系统
  其他几种通信机制基本上都是在同一个计算机上(可以说是同一环境),当然在一些情况下可以实现跨计算机通信。而客户机-服务器系统是不一样的,我的理解是可以当做ip请求,一个客户机请求连接到一台服务器。这种方式在网络上是现在比较流行的,现在比较常用的远程调度,如不RPC(听着很高大上,其实在操作系统上早就有了)还有套接字、socket,这种还是比较常用的,与我们编程紧密相关的,因为你会发现好多的服务需要使用RPC调用。

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

<h1>
channel的使用</h1>

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

<pre><code> 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 } </code></pre>

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

<pre><code> 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 } </code></pre>

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

我们看看channel是如何解决这种问题的:

<pre><code> 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 } </code></pre>

为了看清goroutine执行的步骤和channel的特性,我特意在每一步都做了打印,下面是执行的结果,感兴趣的同学可以自己试试,打印的顺序可能不一样:

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

<pre><code>  打印 "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" 说明主程序结束。 </code></pre>

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

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

到此这篇关于“GO语言之channel”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
go语言并发编程
Go并发编程——channel
图解 Go 并发编程
Go语言中Channel机制
Go 语言为什么这么快,带你详细了解Golang CSP并发模型
Go语言并发--传统锁与channel的选择
Go语言学习3----Go语言特色
初识 Go 语言
Golang学习小结、从入门到精通资料汇总
【文末有惊喜!】一文读懂golang channel

[关闭]
~ ~