GO的利器之---channel

    在GO语言中,channel是一个重要特性,同时也是区别与其它语言的不同之处,一个channel使得并发编程变得简单容易有趣.

   在学习channel之前,我觉得我们有必要百度百科一下进程通信,因为channel其实就是一个处理通信过程的一个东西,首先我们要知

道进程通信大致上可以分为低级进程通信和高级进程通信.(具体了解 百度百科讲的已经是很详细了.)

低级通信

由于进程的互斥和同步,需要在进程间交换一定的信息,故不少学者将它们也归为进程通信。只能传递状态和整数值(控制信息)。

特点:传送信息量小,效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信。

编程复杂:用户直接实现通信的细节,容易出错。

高级通信

提高信号通信的效率,传递大量数据,减轻程序编制的复杂度。

提供三种方式:

1.共享内存模式

2.消息传递模式

3.共享文件模式 

管道通信

是一种信息流缓冲机构, UNIX系统中管道基于文件系统,在内核中通过文件描述符表示。管道以先进先出(FIFO)方式组织数据传输。

实现方法:

调用pipe()函数创建管道

int pipe(int fd[2]);

fd[0]为管道里的读取端

fd[1]则为管道的写入端。

通过write()函数写入信息

int write (int handle,char *buf,unsigned len)

进程通过read()函数读取信息

int read (int handle,void *buf,unsigned len)

特点

1.管道是一个单向通信信道,如果进程间要进行双向通信,通常需要定义两个管道。

2.管道通过系统调用read(), write()函数进行读写操作。

分类

1.匿名管道:只适用于父子进程之间通信;管道能够把信息从一个进程的地址空间拷贝到另一个进程的地址空间。

2.命名管道:命名管道有自己的名字和访问权限的限制,就像一个文件一样。它可以用于不相关进程间的通信,进程通过使用管道的名字获得管道。

理解以上知识之后,我们正式开始介绍  -->GO语言中的channel

channel的概念和语法

            一个channel可以理解为一个先进先出的消息队列。channel用来在协程[goroutine]之间传递数据,准确的说,是用来传递数据的所有权。一个设计良好的程序应该确保同一时刻channel里面的数据只会被同一个协程拥有,这样就可以避免并发带来的数据不安全问题[data races]。

channel的类型

             像数组、切片和字典一样,channel类型是一种组合类型,每一种channel类型都对应着一种简单的数据类型。比如元素的类型是string,那么对应的channel类型就是chan string,进入channel的数据也就必须是string类型的值。

重要特性

    channel类型是可以带有方向的,假设T是一种类型 

   chan T是双向channel类型,编译器允许对双向channel同时进行发送和接收。

   chan<- T  是只写channel类型,编译器只允许往channel里面发送数据. 

   <-chan T   是只读channel类型,编辑器只允许从channel里面接收数据。

双向类型的channel,可以被强制转换成只读channel或者是只写channel,

 但是反过来却不行,只读和只写channel是不可以转换成双向channel的。

channel类型的零值形式称为空channel。一个非空channel类型必须通过make关键字进行创建。例如make(chan int, 10)将会创建出一个可以容纳10个int值的channel。第二个整形的参数值代表的就是channel可以容纳数据的大小,如果不提供这个参数值,那默认值就是零.

例如:

var chan  string;   // nil channel

ch:=make(chan string); // 0 channel 

ch:=make(chan string,10); // 10 channel

 注意:   channel里面的value buffer的容量也就是channel的容量。channel的容量为零表示这是一个阻塞型通道非零表示缓冲型通道[非阻塞型通道]

channel内部结构

每个channel内部实现都有三个队列

1.接收消息的协程队列    这个队列的结构是一个限定最大长度的链表,所有阻塞在channel的接收操作的协程都会被放在这个队列里。

2.发送消息的协程队列    这个队列的结构也是一个限定最大长度的链表。所有阻塞在channel的发送操作的协程也都会被放在这个队列里。

3.环形数据缓冲队列       这个环形数组的大小就是channel的容量。如果数组装满了,就表示channel满了,如果数组里一个值也没有,就表示channel是空的。对于一个阻塞型channel来说,它总是同时处于即满又空的状态。

一个channel被所有使用它的协程所引用,会有两种结果:

一.只要这两个装了协程的队列长度大于零,那么这个channel就永远不会被垃圾回收。

二. 协程本身如果阻塞在channel的读写操作上,这个协程也永远不会被垃圾回收,即使这个channel只会被这一个协程所引用   

channel基本的使用

channel支持以下操作

1.使用cap(ch)函数查询channel的容量,cap是golang的内置函数

2.使用len(ch)函数查询channel内部的数据长度,len函数也是内置的,表面上这个函数很有意义,但实际上它很少用。

3.使用close(ch)关闭channel,close也是内置函数。一个非空channel只能够被关闭一次,如果关闭一个已经被关闭的或者是关闭一个空channel将会引发panic。另外关闭一个只读channel是非法的,编译器直接报错。

4.使用ch <- v发送一个值v到channel。发送值到channel可能会有多种结果,即可能成功,也可能阻塞,甚至还会引发panic,取决于当前channel在什么状态。

5.使用 v, ok <- ch 接收一个值。第二个遍历ok是可选的,它表示channel是否已关闭。接收值只会又两种结果,要么成功要么阻塞,而永远也不会引发panic。

所有的这些操作都是同步的协程安全的,不需要加任何其它同步控制。

通道channel的遍历

for-range语法可以用到通道上。循环会一直接收channel里面的数据,直到channel关闭。不同于array/slice/map上的for-range,channel的for-range只允许有一个变量。

for v = range demoChannel{//逻辑处理}  等价于

 for{ v,ok=<-demoChannel 

       if!ok{

     break

}

//逻辑处理

}

注意,for-range对应的channel不能是只写channel。

Select-Cases

select块是为channel特殊设计的语法,它和switch语法非常相近。分支上它们都可以有多个case块和做多一个default块,但是也有很多不同

select 到 括号{之间不得有任何表达式

fallthrough关键字不能用在select里面

所有的case语句要么是channel的发送操作,要么就是channel的接收操作

select里面的case语句是随机执行的,而不能是顺序执行的。设想如果第一个case语句对应的channel是非阻塞的话,case语句的顺序执行会导致后续的case语句一直得不到执行除非第一个case语句对应的channel里面的值都耗尽了。

如果所有case语句关联的操作都是阻塞的,default分支就会被执行。如果没有default分支,当前goroutine就会阻塞,当前的goroutine会挂接到所有关联的channel内部的协程队列上。 所以说单个goroutine是可以同时挂接到多个channel上的,甚至可以同时挂接到同一个channel的发送协程队列和接收协程队列上。当一个阻塞的goroutine拿到了数据接触阻塞的时候,它会从所有相关的channel队列中移除掉。

超时机制

  超时机制其实也是channel的错误处理,channel固然好用,但是有时难免会出现实用错误,当是读取channel的时候发现channel为空,如果没有错误处理,像这种情况就会使整个goroutine锁死了,无法运行,channel 并没有直接处理超时的方法,可以使用select机制处理,select的特点比较明显,只要有一个case完成了程序就会往下运行,利用这种方法,可以实现channel的超时处理.

channel规则

    空channel

      1.关闭一个空channel会导致当前goroutine引发panic

      2.向一个空channel发送值会导致当前的goroutine阻塞

      3.从一个空channel接收值也会导致当前的goroutine阻塞

      4.在空channel上的调用len和cap函数都统一返回零。

    已关闭的Channel

      1.关闭一个已关闭的channel会引发panic

      2.向一个已关闭的channel发送值会引发panic。当这种send操作处于select块里面的case语句上时,它会随时导致select语句引发panic。

       3.从一个已关闭的channel上接收值既不会阻塞也不能panic,它一直能成功返回。只是返回的第二个值ok永远是false,表示接收到的v是在channel关闭之后拿到的,对应得值也是相应元素类型的零值。可以无限循环从已关闭的channel上接收值。

活跃的Channel

一. 关闭操作

1.从channel的接收协程队列中移除所有的goroutine,并唤醒它们。

2.从channel的接收协程队列中移除所有的goroutine,并唤醒它们。

3.一个已关闭的channel内部的缓冲数组可能不是空的,没有接收的这些值会导致channel对象永远不会被垃圾回收。

二.发送操作

1.如果是阻塞型channel,那就从channel的接收协程队列中移出第一个协程,然后把发送的值直接递给这个协程。

2.如果是阻塞型channel,并且channel的接收协程队列是空的,那么当前的协程将会阻塞,并进入到channel的发送协程队列里。

3.如果是缓冲型channel,并且缓冲数组里还有空间,那么将发送的值添加到数组最后,当前协程不阻塞。

4.如果是缓冲型channel,并且缓冲数组已经满了,那么当前的协程将会阻塞,并进入到channel的发送协程队列中。

三.接收操作

1.如果是缓冲型channel,并且缓冲数组有值,那么当前的协程不会阻塞,直接从数组中拿出第一个值。如果发送队列非空,还需要将队列中的第一个goroutine唤醒。

2.如果是阻塞型channel,并且发送队列非空的话,那么唤醒发送队列第一个协程,该协程会将发送的值直接递给接收的协程。

3.如果是缓冲型channel,并且缓冲数组为空,或者是阻塞型channel,并且发送协程队列为空,那么当前协程将会阻塞,并加入到channel的接收协程队列中。

总结

1.如果channel关闭了,那么它的接收和发送协程队列必然空了,但是它的缓冲数组可能还没有空。

2.channel的接收协程队列和缓冲数组,同一个时间必然有一个是空的

3.channel的缓冲数组如果未满,那么它的发送协程队列必然是空的

4.对于缓冲型channel,同一时间它的接收和发送协程队列,必然有一个是空的

5.对于非缓冲型channel,一般来说同一时间它的接收和发送协程队列,也必然有一个是空的,但是有一个例外,那就是当它的发送操作和接收操作在同一个select块里出现的时候,两个队列都不是空的。

使用:

package main

import (
	"fmt"
	"strconv"
)

var count int = 0

func main(){
	for i := 0; i < 3; i++  {
        go test()
	}

}

func test(){
	count++
	fmt.Print( "count的值为:" + strconv.Itoa(count))
}


运行main方法,一般来说 我们肯定知道会打印出count的值,但是你会发现控制台什么都没有,也就意味这这个方法压根就没有执行,但是你肯定疑问,我明明在main方法中调用了呀,为什么没有执行,

  原因:

   Go程序执行main()函数 ,当main()函数返回时,程序就会退出,主程序并不等待其他goroutine的,导致没有任何输出。

那么,既然 不等待,我们是不是可以设置个条件让他等待呢,如下代码所示:

package main

import (
	"fmt"
	"strconv"
	"time"
)

var count int = 0

func main(){
	for i := 0; i < 3; i++  {
        go test()
	}
	time.Sleep(1e9)

}

func test(){
	count++
	fmt.Print( "count的值为:" + strconv.Itoa(count))
}


在看打印台:

会发现出现值了,但是这个有一个很大的隐患,就是 无法保证我们使用的是同一个,  一般解决办法就是各种加锁,解锁,这是很复杂的工程,而channel就可以很方便的解决这个问题.我们修改上面的代码,

package main

import (
	"fmt"
	"runtime"
)

var count int = 0

func main() {
	maxProcs := runtime.NumCPU() // 获取cpu个数
	fmt.Println(maxProcs)
	runtime.GOMAXPROCS(maxProcs) //限制同时运行的goroutines数量
	timeouts := make([]chan int, 3)
	for i := 0; i < 3; i++ {
		timeouts[i] = make(chan int)
		fmt.Println(i, "的线程开始")
		go test(i, timeouts[i])
		fmt.Println(i, "的线程结束")
	}
	fmt.Println("开始进入读取")
	for num, ch := range timeouts {
		fmt.Println(num, "的读取开始")
		<-ch  //接收值
		fmt.Println(num, "的读取结束")

	}
	//time.Sleep(1e9)
	fmt.Println("主程序结束")

}

func test(i int, ch chan int) {
	fmt.Println(i, "的写入操作开始")
	ch <- 1 //发送一个值到channel
	fmt.Println(i, "的写入操作结束")
}

在理解下面分析的结果,请先记住下面这段话,方便理解:(重点)

1.向channel写入数据时会导致程序阻塞,直到有其他goroutine从这个channel中读取数据,

2.channel中读取未写入的数据会导致程序阻塞,直到这个channel中被写入了该数据为止 .

打印结果查看并分析(每次打印结果会有所不同,但是分析都是这样分析的,主要理解读取阻塞和写入阻塞):

如果我们想要上面所有程序 结束再结束主程序,那么我们让主程序稍微等待一下就行.打开上面的注释代码 

time.Sleep(1e9)

然后打印查看结果:

到此,关于GO的channel完毕,如果有啥解释不对的, 欢迎指出, 一定虚心接受并改正.

猜你喜欢

转载自blog.csdn.net/FindHuni/article/details/105805523