channel

Posted 罗夏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了channel相关的知识,希望对你有一定的参考价值。

一.基本语法

c := make(chan bool) //创建一个无缓冲的bool型Channel
c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false
ch chan int //可读写
ch1 chan<- int  //ch1只能写
ch2 <-chan int  //ch2只能读
channel是类型相关的,也就是一个channel只能传递一种类型

  

二.为什么要使用channel

goroutine是Go语言中的轻量级线程实现,由Go运行时(runtime)管理.
先看一个例子:

func Sub(i int) {
	fmt.Println("from sub func", i)
}

func main() {
	for i := 0; i < 5; i++ {
		Sub(i)
	}
	fmt.Println("from main")
}

这个例子做了一件事情,在main函数中串行执行了5次Sub函数.

如果我们需要Sub函数能够并发的执行,我们加个go,将每一个Sub函数放在goroutine中去(main函数其实也是个goroutine),代码如下所示:

func Sub(i int) {
	fmt.Println("from sub func", i)
}

func main() {
	for i := 0; i < 5; i++ {
		go Sub(i)
	}
	fmt.Println("from main")
}

  

编译执行,你会发现只打印出了from main,Sub函数中字符并没有打印出来.这是因为主函数main启动了5个Sub函数后,并没有等待它们完成即退出了!这显然不是我们要的结果,我们使用go提供的消息通信机制channel来重构代码,保证Sub函数执行完成后,主函数再退出!

 

三.无缓存channel(信道,这个翻译较为接近其英文表达的意思)

channel(信道)是什么,简单说,是goroutine之间互相通讯的东西。类似我们Unix上的管道(可以在进程间传递消息), 用来goroutine之间发消息和接收消息。其实,就是在做goroutine之间的内存共享。

func Sub(ch chan int) {
	for i := 0; i < 5; i++ {
		fmt.Println("from sub func", i)
	}
	ch <- 1 // 向channel存消息,如果没有其他goroutine取走数据,那么挂起Sub
}

func main() {
	ch := make(chan int) // 创建了一个无缓存的channel
	go Sub(ch) // 开启一个goroutine来执行Sub
	fmt.Println("from main")
	<-ch // 从channel取消息,如果没有写入消息,挂起main
}

  

无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine.

所以上面代码的执行流程是:
创建ch信道
新开goroutine来执行Sub,但因为ch信道中写入的消息没有被取走,Sub挂起.
from main被打印出来.
<-ch 挂起了main,直到取到数据,这就保证了Sub完成后,main才结束.

所以无缓存信道,存入数据必须取走,只存不取或取空的数据,都将导致死锁.

以下情形都是死锁:

//取不存在的消息:
ch := make(chan int)
<-ch

//只写不取:
ch := make(chan int)
ch <- 1

//多个channel,ch1等待ch2的消息,但ch2没有消息写入:
ch1 := make(chan int)
ch2 := make(chan int)
ch1 <- <-ch2
<-ch1

  

四.带缓存的channel

无缓存信道只负责流通消息,任何对该信道的读和写,都阻塞信道.
带缓存的channel,不仅可以流通数据,还可以缓存数据,只有达到缓存最大数目后,也就是缓存满了后,才阻塞信道,这听起来很像队列(Queue).其实,缓冲信道是先进先出的,我们可以把缓冲信道看作为一个线程安全的队列

ch := make(chan int, 2) // 带缓存channel,缓存数目2个,放入2个数据,不会挂起,只有放入第3个的时候才挂起当前goroutine

示例代码:

 

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

  

对ch的写入,因为没有超过缓存数目3,所以不会阻塞;对ch取数据,将按先进先出,依次输出1 2 3

你会发现,带缓存的信道,取数据还是挺麻烦的.我们用for range来取数据

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 4

	for v := range ch {
		fmt.Println(v)
	}
}

  

报出deadlock,原因是ch没有关闭的情形下,range一直在读取.加一个判断:

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 4

	for v := range ch {
		fmt.Println(v)
		if len(ch) <= 0 {
			break
		}
	}
}

  

我们判断了ch中有没有数据,没有数据就跳出循环.可以正常输出.当然我们也可以显式关闭信道.

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 4

	close(ch) // 关闭信道

	for v := range ch {
		fmt.Println(v)
	}
}

  

五.执行多个goroutine

1.无缓存

const MAX = 1000

var ch chan int

func Sub(i int) {
	fmt.Println(i)
	ch <- 1
}

func main() {
	ch = make(chan int)
	for i := 0; i < MAX; i++ {
		go Sub(i)
	}

	for i := 0; i < MAX; i++ {
		<-ch
	}
}

  

2.带缓存

const MAX = 1000

var ch chan int

func Sub(i int) {
	fmt.Println(i)
	ch <- 1
}

func main() {
	ch = make(chan int, MAX)
	for i := 0; i < MAX; i++ {
		go Sub(i)
	}

	for i := 0; i < MAX; i++ {
		<-ch
	}
}

  

两者效果相同,不同点在于:
无缓冲的信道是一批数据一个一个的流进流出
缓冲信道则是一个一个存储,然后一起流出去

 

六.单向channel

顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数据。同理,如果一个channel只允许写,即使写进去了,也没有丝毫意义,因为没有机会读取里面的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。

ch1 chan<- int //ch1只能写
ch2 <-chan int //ch2只能读

示例:
func Parse(ch <-chan int) {
	for value := range ch {
		fmt.Println("Parsing value", value)
	}
}

  

 

以上是关于channel的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 协程Channel 通道 ④ ( Channel 通道的热数据流属性 | Channel 通道关闭过程 | Channel 通道关闭代码示例 )

FFmpeg拼接文件时报错channel element 1.0 is not allocated的分析思路和解决方法

FFmpeg拼接文件时报错channel element 1.0 is not allocated的分析思路和解决方法

FFmpeg拼接文件时报错channel element 1.0 is not allocated的分析思路和解决方法

FFmpeg拼接文件时报错channel element 1.0 is not allocated的分析思路和解决方法

Channel 的死锁