Golang 通道(channel)学习一

通道-channel是Golang中唯一自带的可以满足并发安全性的类型。

通道的作用:协程间安全的传输数据。这里的协程也可以粗略的理解为线程,但是其实粒度比线程更小,更少的占用系统资源,所以go中的协程在一台机器上就可以启动十几万甚至几十万个。

我们传统的在线程间共享数据的方式就是:共享内存。但是,Golang的设计者并不认为这是一种好的设计方式(虽然Go语言中也支持这种并发处理方式的编码)。Go语言有了另一种更好的解决方法:那就是通道,核心思想就是通过数据传输来共享数据,而不是通过内存来共享。

传统的并发安全方式大多都是加锁的方式,synchronized,lock等,不管是怎样优化,都是锁住临界区的数据,控制同一时间访问竞争资源的线程数量,来保证数据安全。(临界区:只的是多线程并发修改的一块内存区域或内存数据)

Golang通过数据传输来控制竞争资源,则更加有优势,更加有效率,并且对于go的开发人员来说,编写并发代码也更加简单。

通道是什么样的

通道其实就是一个队列,是先进先出的队列(FIFO),先进入的数据会先被接收端取走,并一直严格按照进入的先后顺序来处理;并且Golang会自动保证数据读写的完整性,不会出现读取或写入半个元素的情况,这里也就是保证了读和写的原子性。

通道的使用方式:

向通道写入数据:
通道 <- 数据

从通道接收数据:
接收变量 <- 通道
例如:v :=<-chans
for循环的写法 for u :=range chans {…}

通道使用make来创建,如下:

c:= make(chan string, 0)

声明的类型,限制了通道能传输的数据类型,类似这里的声明,通道里只能接受string类型的数据。第二个参数是可选的,表示通道的容量,当设置大于0时,通道可以缓存多个元素数据。如果第二个参数为空或不写,那么就是非缓冲通道,如果设置大于0 ,就是缓冲通道。

这两种的区别在于:非缓冲通道是阻塞的,当通道被写入数据后,之后通道中的数据被读取走,才能再次向通道中写入数据,如果数据一直没有被读取,那么再次写入的程序将被阻塞在写入时,直到能够写入或超时。

缓冲通道是非阻塞的,如果缓冲通道没有被写满,那么就可以一直向通道中写入数据,当缓冲通道被写满后,此时写入端也会被阻塞。从通道读取数据一是一样的,如果通道为空,那么从通道中读取数据的操作也会被阻塞。

还注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本。

package main

import "fmt"

func main() {
   ints := make(chan int, 2)
   ints <- 1
   ints <- 2

   fmt.Println(<-ints)
   fmt.Println(<-ints)

   ints <- 3
   fmt.Println(<-ints)
}

输出结果:

1
2
3

这里如果把ints <- 3,换到上面的位置,就会生成线程死锁,代码如下:

package main

import "fmt"

func main() {
   ints := make(chan int, 2)
   ints <- 1
   ints <- 2
   ints <- 3 //注意这里
   fmt.Println(<-ints)
   fmt.Println(<-ints)

   fmt.Println(<-ints)
}

输出结果:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	D:/GO_PATH/src/gostudy/src/cgss.go:9 +0xa2

为什么会这样?
正如上面说的,通道会产生阻塞,这里即便是带缓冲的通道,但是通道中的数据没有被消费掉,又要写入3,还因为这里的写入和消费都是在一个协程里进行的,所以就产生了阻塞,就产生了阻塞。

总结:

1、按是否缓冲分
缓冲通道
var c = make(chan int,10) 正常声明的通道
发送时,只有通道塞满才阻塞;接收时,只有通道空了才阻塞
非缓冲通道 var c = make(chan int) 正常声明的通道
只有发送和接收同时进行,才不阻塞,缺少其中一个操作即阻塞

2、通道是并发安全的
读写都是原子性的