Golang 管道(Channel)
优质
小牛编辑
143浏览
2023-12-01
上述实现并发的代码中为了保持主线程不挂掉,我们都会在最后写上一个死循环或者写上一个定时器来实现等待 goroutine 执行完毕
上述实现并发的代码中为了解决生产者消费者资源同步问题,我们利用加锁来解决,但是这仅仅是一对一的情况,如果是一对多或者多对多,上述代码还是会出现问题
综上所述,企业开发中需要一种更牛 X 的技术来解决上述问题,那就是管道(Channel)
Channel 的本质是一个队列
Channel 是线程安全的,也就是自带锁定功能
Channel 声明和初始化
- 声明:
var 变量名chan 数据类型
- 初始化:
mych := make(chan 数据类型, 容量)
- Channel 和切片还有字典一样,必须 make 之后才能使用
- Channel 和切片还有字典一样,是引用类型
package main import "fmt" func main() { // 1.声明一个管道 var mych chan int // 2.初始化一个管道 mych = make(chan int, 3) // 3.查看管道的长度和容量 fmt.Println("长度是", len(mych), "容量是", cap(mych)) // 4.像管道中写入数据 mych<- 666 fmt.Println("长度是", len(mych), "容量是", cap(mych)) // 5.取出管道中写入的数据 num := <-mych fmt.Println("num = ", num) fmt.Println("长度是", len(mych), "容量是", cap(mych)) }
注意点:
- 管道中只能存放声明的数据类型,不能存放其它数据类型
- 管道中如果已经没有数据,再取就会报错
- 如果管道中数据已满,再写入就会报错
package main import "fmt" func main() { // 1.声明一个管道 var mych chan int // 2.初始化一个管道 mych = make(chan int, 3) // 注意点: 管道中只能存放声明的数据类型, 不能存放其它数据类型 //mych<-3.14 // 注意点: 管道中如果已经没有数据, // 并且检测不到有其它协程再往管道中写入数据, 那么再取就会报错 //num = <-mych //fmt.Println("num = ", num) // 注意点: 如果管道中数据已满, 再写入就会报错 mych<- 666 mych<- 777 mych<- 888 mych<- 999 }
管道的关闭和遍历
package main import "fmt" func main() { // 1.创建一个管道 mych := make(chan int, 3) // 2.往管道中存入数据 mych<-666 mych<-777 mych<-888 // 3.遍历管道 // 第一次遍历i等于0, len = 3, // 第二次遍历i等于1, len = 2 // 第三次遍历i等于2, len = 1 //for i:=0; i<len(mych); i++{ // fmt.Println(<-mych) // 输出结果不正确 //} // 3.写入完数据之后先关闭管道 // 注意点: 管道关闭之后只能读不能写 close(mych) //mych<- 999 // 报错 // 4.遍历管道 // 利用for range遍历, 必须先关闭管道, 否则会报错 //for value := range mych{ // fmt.Println(value) //} // close主要用途: // 在企业开发中我们可能不确定管道有还没有有数据, 所以我们可能一直获取 // 但是我们可以通过ok-idiom模式判断管道是否关闭, 如果关闭会返回false给ok for{ if num, ok:= <-mych; ok{ fmt.Println(num) }else{ break; } } fmt.Println("数据读取完毕") }
Channel 阻塞现象
- 单独在主线程中操作管道,写满了会报错,没有数据去获取也会报错
- 只要在协程中操作管道过,写满了就会阻塞,没有就数据去获取也会阻塞
package main import ( "fmt" "time" ) // 创建一个管道 var myCh = make(chan int, 5) func demo() { var myCh = make(chan int, 5) //myCh<-111 //myCh<-222 //myCh<-333 //myCh<-444 //myCh<-555 //fmt.Println("我是第六次添加之前代码") //myCh<-666 //fmt.Println("我是第六次添加之后代码") fmt.Println("我是第六次直接获取之前代码") <-myCh fmt.Println("我是第六次直接获取之后代码") } func test() { //myCh<-111 //myCh<-222 //myCh<-333 //myCh<-444 //myCh<-555 //fmt.Println("我是第六次添加之前代码") //myCh<-666 //fmt.Println("我是第六次添加之后代码") //fmt.Println("我是第六次直接获取之前代码") //<-myCh //fmt.Println("我是第六次直接获取之后代码") } func example() { time.Sleep(time.Second * 2) myCh<-666 } func main() { // 1.同一个go程中操作管道 // 写满了会报错 //myCh<-111 //myCh<-222 //myCh<-333 //myCh<-444 //myCh<-555 //myCh<-666 // 没有了去取也会报错 //<-myCh // 2.在协程中操作管道 // 写满了不会报错, 但是会阻塞 //go test() // 没有了去取也不会报错, 也会阻塞 //go test() //go demo() //go demo() // 3.只要在协程中操作了管道, 就会发生阻塞现象 go example() fmt.Println("myCh之前代码") <-myCh fmt.Println("myCh之后代码") //for{ // ; //} }
利用 Channel 实现生产者消费者
package main import ( "fmt" "math/rand" "time" ) // 定义缓冲区 var myCh = make(chan int, 5) var exitCh = make(chan bool, 1) // 定义生产者 func producer(){ rand.Seed(time.Now().UnixNano()) for i:=0;i<10;i++{ num := rand.Intn(100) fmt.Println("生产者生产了: ", num) // 往管道中写入数据 myCh<-num //time.Sleep(time.Millisecond * 500) } // 生产完毕之后关闭管道 close(myCh) fmt.Println("生产者停止生产") } // 定义消费者 func consumer() { // 不断从管道中获取数据, 直到管道关闭位置 for{ if num, ok := <-myCh; !ok{ break }else{ fmt.Println("---消费者消费了", num) } } fmt.Println("消费者停止消费") exitCh<-true } func main() { go producer() go consumer() fmt.Println("exitCh之前代码") <-exitCh fmt.Println("exitCh之后代码") }
无缓冲 Channel
package main import "fmt" var myCh1 = make(chan int, 5) var myCh2 = make(chan int, 0) func main() { // 有缓冲管道 // 只写入, 不读取不会报错 //myCh1<-1 //myCh1<-2 //myCh1<-3 //myCh1<-4 //myCh1<-5 //fmt.Println("len =",len(myCh1), "cap =", cap(myCh1)) // 无缓冲管道 // 只有两端同时准备好才不会报错 go func() { fmt.Println(<-myCh2) }() // 只写入, 不读取会报错 myCh2<-1 //fmt.Println("len =",len(myCh2), "cap =", cap(myCh2)) // 写入之后在同一个线程读取也会报错 //fmt.Println(<-myCh2) // 在主程中先写入, 在子程中后读取也会报错 //go func() { // fmt.Println(<-myCh2) //}() }
无缓冲 Channel 和有缓冲 Channel
- 有缓冲管道具备异步的能力(写几个读一个或读几个)
- 无缓冲管道具备同步的能力(写一个读一个)
package main import ( "fmt" "math/rand" "time" ) // 定义缓冲区 //var myCh = make(chan int, 0) var myCh = make(chan int) var exitCh = make(chan bool, 1) // 定义生产者 func producer(){ rand.Seed(time.Now().UnixNano()) for i:=0;i<10;i++{ num := rand.Intn(100) fmt.Println("生产者生产了: ", num) // 往管道中写入数据 myCh<-num //time.Sleep(time.Millisecond * 500) } // 生产完毕之后关闭管道 close(myCh) fmt.Println("生产者停止生产") } // 定义消费者 func consumer() { // 不断从管道中获取数据, 直到管道关闭位置 for{ if num, ok := <-myCh; !ok{ break }else{ fmt.Println("---消费者消费了", num) } } fmt.Println("消费者停止消费") exitCh<-true } func main() { go producer() go consumer() fmt.Println("exitCh之前代码") <-exitCh fmt.Println("exitCh之后代码") }
IO 的延迟说明:看到的输出结果和我们想象的不太一样,是因为IO输出非常消耗性能,输出之后还没来得及赋值可能就跑去执行别的协程了
单向管道和双向管道
- 默认情况下所有管道都是双向了(可读可写)
- 但是在企业开发中, 我们经常需要用到将一个管道作为参数传递
- 在传递的过程中希望对方只能单向使用, 要么只能写,要么只能读
双向管道
var myCh chan int = make(chan int, 0)
单向管道
var myCh chan<- int = make(chan<- int, 0) var myCh <-chan int = make(<-chan int, 0)
注意点:
- 双向管道可以自动转换为任意一种单向管道
- 单向管道不能转换为双向管道
package main import "fmt" func main() { // 1.定义一个双向管道 var myCh chan int = make(chan int, 5) // 2.将双向管道转换单向管道 var myCh2 chan<- int myCh2 = myCh fmt.Println(myCh2) var myCh3 <-chan int myCh3 = myCh fmt.Println(myCh3) // 3.双向管道,可读可写 myCh<-1 myCh<-2 myCh<-3 fmt.Println(<-myCh) // 3.只写管道,只能写, 不能读 // myCh2<-666 // fmt.Println(<-myCh2) // 4.指读管道, 只能读,不能写 fmt.Println(<-myCh3) //myCh3<-666 // 注意点: 管道之间赋值是地址传递, 以上三个管道底层指向相同容器 }
单向管道作为函数参数
package main import ( "fmt" "math/rand" "time" ) // 定义生产者 func producer(myCh chan<- int){ rand.Seed(time.Now().UnixNano()) for i:=0;i<10;i++{ num := rand.Intn(100) fmt.Println("生产者生产了: ", num) // 往管道中写入数据 myCh<-num //time.Sleep(time.Millisecond * 500) } // 生产完毕之后关闭管道 close(myCh) fmt.Println("生产者停止生产") } // 定义消费者 func consumer(myCh <-chan int) { // 不断从管道中获取数据, 直到管道关闭位置 for{ if num, ok := <-myCh; !ok{ break }else{ fmt.Println("---消费者消费了", num) } } fmt.Println("消费者停止消费") } func main() { // 定义缓冲区 var myCh = make(chan int, 5) go producer(myCh) consumer(myCh) }