ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 简介 * 读写类型:`chan int` * 只读类型:`<-chan int`,叫做receive-only * 只写类型:`chan<- int`,叫做send-only channel支持3种类型(通过%T看到的) * len是未读取的数据 * cap是容量 * 无缓冲区,会同步起来 * 有缓冲区就像发短信 1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。 2. 发送操作和接收操作中对元素值的处理都是不可分割的。 3. 发送操作在完全完成之前会被阻塞。接收操作也是如此 元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是在接收操作符右边的那个元素值,而是它的副本 协程运行在相同的地址空间,因此访问共享内存必须做好同步.协程奉行**通过通信来共享内存,而不是共享内存来通信** 引用类型channel是csp模式的具体实现,用于多个协程通讯.内部实现了同步,确保并发安全 # 互斥性 发送操作包括了“复制元素值”和“放置副本到通道内部”这两个步骤。 在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。也就是说,在它之后的代码不会有执行的机会,直到这句代码的阻塞解除。 更细致地说,在通道完成发送操作之后,运行时系统会通知这句代码所在的 goroutine,以使它去争取继续运行代码的机会。 另外,接收操作通常包含了“复制通道内的元素值”“放置副本到接收方”“删掉原值”三个步骤。 在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。 说到这里,你可能已经感觉到,如此阻塞代码其实就是为了实现操作的互斥和元素值的完整 # 阻塞性 先说针对缓冲通道的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有元素值被接收走。 这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。 由于发送操作在这种情况下被阻塞后,它们所在的 goroutine 会顺序地进入通道内部的发送等待队列,所以通知的顺序总是公平的。 相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。 因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等待队列。 对于非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。也就是说,只有收发双方对接上了,数据才会被传递。 并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲通道则在用异步的方式传递数据 **对于值为nil的通道,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态。它们所属的 goroutine 中的任何代码,都不再会被执行。** **注意,由于通道类型是引用类型,所以它的零值就是nil。换句话说,当我们只声明该类型的变量但没有用make函数对它进行初始化时,该变量的值就会是nil。我们一定不要忘记初始化通道!** # channel类型 和map类似,channel也是一个对应make创建的底层数据结构的引用 当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel的引用,因此调用者何被调用这将引用同一个channel对象.和其他的引用类型一样,channel的零值也是nil 定义有一个channel时,需要定义发送到channel的值的类型.channel可以使用内置的make()函数来创建 ~~~ make(chan Type) //等价于make(chan Type, 0) make(chan Type, capacity) ~~~ 当capacity等于0时,channel是无缓冲阻塞读写的, 当capacity>0时,channel有缓冲,是非阻塞的,直到写满capacity个元素才阻塞写入 channel通过操作符`<-`来接收和发送数据,发送和接收数据语法: ~~~ channel <- value //发送value到channel <- channel //接收并将其丢弃 x := <-channel //从channel中接收数据,并赋值给x x, ok := <-channel //功能同上,同时检查通道是否已关闭或者为空 ~~~ **默认情况下,channel接收和发送数据都是阻塞的**,除非另一端已经准备好了,这样就使得协程同步变得简单,而不需要显示的lock # 解决资源争抢 ~~~ //全局变量,创建一个channel var ch = make(chan int) //定义一个打印机 //打印机属于公共资源 func Printer(str string) { for _, data := range str{ fmt.Printf("%c", data) time.Sleep(time.Second) } fmt.Printf("\n") } //p1执行完后才到p2 func Person1() { Printer("hello") //给管道写数据 ch <- 666 } func Person2() { //从管道取数据,通道没数据就阻塞 <-ch Printer("world") } func main() { //新建2个协程,代表2个人,2个人同时使用打印机 go Person1() go Person2() //特意不让主协程结束,死循环 for { } } ~~~ # 实现同步和数据交互 ~~~ func main() { //创建channel ch := make(chan string) defer fmt.Println("主协程也结束") go func() { defer fmt.Println("子协程调用完毕") for i := 0; i < 2; i++ { fmt.Println("子协程 i = ", i) time.Sleep(time.Second) } //给主协程发送数据 ch <- "我是子协程,工作完毕" }() //小心会永远阻塞 str := <-ch //没有数据前会阻塞 fmt.Println("str = ", str) } ~~~ # 无缓冲的channel 本身是不存放东西 无缓存的channel是指在接收前没有能力保存任何值的通道 这种类型的通道要求发送协程和接收协程同时准备好,才能完成发送和接收操作. 如果两个协程没有同时准备好,通道会导致先执行发送或接收操作的协程阻塞等待 这种对通道进行发送和接收的交互行为本身就是同步的.其中任意一个操作都无法离开另一个操作单独存在 无缓存的channel格式 ~~~ make(chan Type) //等价于make(chan Type, 0) ~~~ 如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和接收者准备好接收 ~~~ func main() { //创建channel,无缓存的 ch := make(chan int, 0) //len(ch)缓冲区剩余数据个数,cap(ch)缓冲区大小 fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch)) //新建协程 go func() { for i := 0; i < 3 ; i++ { fmt.Println("子协程: ", i) ch <- i //往channel写东西 fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch)) } }() //延迟 time.Sleep(time.Second*2) for i := 0; i < 3; i++ { num := <-ch //channel没内容前会阻塞 fmt.Println("主协程: ", num) } } ~~~ 输出 ~~~ len(ch) = 0, cap(ch) = 0 子协程: 0 主协程: 0 len(ch) = 0, cap(ch) = 0 子协程: 1 len(ch) = 0, cap(ch) = 0 子协程: 2 主协程: 1 主协程: 2 ~~~ # 有缓冲的channel 有缓存的channel格式 ~~~ make(chan Type, capacity) ~~~ 如果给定一个缓冲区容量,通道就是异步.只要缓冲区有没有使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞的进行 ~~~ func main() { //创建channel ch := make(chan int, 3) //len(ch)缓冲区剩余数据个数,cap(ch)缓冲区大小 fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch)) //新建协程 go func() { for i := 0; i < 10 ; i++ { fmt.Println("子协程: ", i) ch <- i //往channel写东西 fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch)) } }() //延迟 time.Sleep(time.Second*2) for i := 0; i < 3; i++ { num := <-ch //channel没内容前会阻塞 fmt.Println("主协程: ", num) } } ~~~ 输出 ~~~ len(ch) = 0, cap(ch) = 3 子协程: 0 len(ch) = 1, cap(ch) = 3 子协程: 1 len(ch) = 2, cap(ch) = 3 子协程: 2 len(ch) = 3, cap(ch) = 3 子协程: 3 主协程: 0 len(ch) = 2, cap(ch) = 3 子协程: 4 len(ch) = 3, cap(ch) = 3 子协程: 5 主协程: 1 主协程: 2 len(ch) = 3, cap(ch) = 3 ~~~ # 关闭channel **只有发送者可以关闭管道,接收者不能关闭管道** 关闭channel无法发送数据,发送会报错,可以读取 ~~~ close(channel) ~~~ 写端关闭,可以从中读到数据0,说明写端关闭了 判断是否关闭channel ~~~ if num, ok := <-ch; ok ==true //false关闭 ~~~ ~~~ func main() { //创建channel ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i //写数据 } //不需要写数据了,关闭channel close(ch) }() for { //如果ok为true,说明管道没关闭 if num, ok := <-ch; ok == true { fmt.Println("num = ", num) } else { //管道关闭 break } } } ~~~ 输出 ~~~ num = 0 num = 1 num = 2 num = 3 num = 4 ~~~ # range遍历channel内容 ~~~ func main() { //创建channel ch := make(chan int) go func() { for i := 0; i < 5; i++ { ch <- i //写数据 } //不需要写数据了,关闭channel close(ch) }() for num := range ch { fmt.Println("num: ", num) } } ~~~ # 单向channel 默认情况下,通道是双向的,也就是说,既可以往里面发送数据也可以同里面接收数据 但是,我们经常看见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向 单向channel变量的声明非常简单,如下 ~~~ var ch1 chan int //ch1是一个正常的channel,不是单向的 var ch2 chan<-float64 //ch2是单向的channel,只用于写float64数据,只写 var ch3<-chan int //ch3是单向channel,只用于读取int数据,只读 ~~~ * `chan<-`表示数据进入管道,要把数据写进管道,对于调用者就是输出 * `<-chan`表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入 可以将channel隐式转换为单向队列,只收或只发,**不能将**单向channel转换为普通channel: ~~~ func main() { //创建channel ch := make(chan int) //双向channel能隐式转换为单向channel var writeCh chan<-int = ch //只能写,不能读 var readCh <-chan int = ch //只能读,不能写 //写 writeCh <- 666 //读 <-readCh //单向无法转换为双向 } ~~~ ## 生产者和消费者 ~~~ func producer(out chan<-int) { for i := 0; i <10; i++ { out <- i*i } close(out) } func consumer(in <-chan int) { for num := range in{ fmt.Println("num = ", num) } } func main() { //创建channel ch := make(chan int) //生产者,生产数字,写入channel go producer(ch) //channel传参数,引用传递 //消费者,从channel读取内容,打印 consumer(ch) } ~~~