一篇文章带你了解Go语言基础之并发(channel)

开发 前端
本篇继续带来Go语言并发基础,channel如何使用。看看Go协程如何配合channel。

[[360253]]

 前言

Hi,大家好,我是码农,星期八,本篇继续带来Go语言并发基础,channel如何使用。

看看Go协程如何配合channel。

为什么需要channel

channel在Go中,也叫做管道,是用来多线程之间共享数据的。

通常情况下,在Go中共享数据用的也是channel,但是在Go有两种共享数据方式。

  • 共享内存实现通讯。
  • 通过管道(channel)通讯(推荐)。

为啥子共享内存通讯不太推荐?

示例代码:多线程修改一个值。

函数

  1. func Calc() { 
  2.     defer wg.Done() 
  3.     NUM = NUM - 1 

main

  1. var NUM = 100 
  2. var wg sync.WaitGroup 
  3.  
  4. func main() { 
  5.     for i := 0; i<100;i++  { 
  6.         wg.Add(1) 
  7.         go Calc() 
  8.     wg.Wait() 
  9.     fmt.Println(NUM) 

执行结果


没错,是2,懵了吧,哈哈哈,理论应该是0才对呀。

这是为啥?

这就是共享内存不太推荐的原因,我们的代码已经是多线程了。

在第一个函数代码中,第3行,NUM = NUM - 1。

如果多个线程同时执行到这一行,并且没有加锁,就会出现数据错乱。

那该怎么做呢?

加锁,加锁可以保证某一段代码只能被一个线程执行,防止被争抢。

代码

  1. func Calc() { 
  2.     defer wg.Done() 
  3.     mutex.Lock() 
  4.     NUM = NUM - 1 
  5.     mutex.Unlock() 

第3行加锁,第5行解锁。

执行结果


这次真的是0的,不管执行几次。

但是会发现一个问题,如果采用这种方式,需要常常注意竞争问题。

所以不是太推荐,需要考虑的比较多,并且各种加锁会消耗性能。

channel语法

channel格式

  1. var 变量名 chan 类型 
  2. 例如 
  3. var x1 chan int //x1管道里面只能存int类型数据 
  4. var x2 chan string //x2管道里面只能存字符串类型数据 

注意


定义管道时,chan int是一个整体,别搞错了各位。

创建channel

创建channel,只能通过make创建。

格式

  1. var 变量名 = make(chan 类型,[管道大小]) 
  2. 示例 
  3. var chan1 = make(chan int,10)//管道可以放10个int元素 
  4. var chan2 = make(chan string,5)//管道可以放5个string元素 

channel操作

创建一个管道。

  1. ch = make(chan int,10) 

channel是一个管道,就像一个管子。

所以可以像管子里面塞东西,并且能取东西,关闭管道就是这个管道不能用了,里面的值取完就打样了。

像管子塞东西(发送)ch <- 666。

从管子取东西(接收)var x = <- ch。

关闭管子close(ch)。

注意:channel是先入先出结构,就像这样。

 

注意事项:

  • 如果通道塞满了,再塞 会阻塞住。
  • 如果通道关闭了,是不能再塞值了,否则会panic。
  • 即使通道关闭了,依然可以取值,直到将管道的值取完,取完后得到的是对应类型零值。
  • 管道不能重复关闭,重复关闭会panic。

无缓冲管道

无缓冲就是这个管道没有长度,就像这样。

就像快读员没有快递柜,需要直接将快递给客户,如果没人要就撂摊子。


示例代码

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6. //模拟张三 
  7. func 张三(x chan string) { 
  8.     var a = <-x 
  9.     fmt.Println(a) 
  10.  
  11. func main() { 
  12.     //通道没有长度,就是无缓冲通道 
  13.     var x = make(chan string) 
  14.     go 张三(x) 
  15.     x <- "张三的快递" 
  16.     fmt.Println("张三快递交付成功"

第16行写入一个值,同理,张三就要等着去接,如果没人接,那就完了。

假设注释第9行代码。

直接报错,all goroutines are asleep - deadlock!,这句话的意思是所有的协程都睡着了,死锁

无缓冲说明通道长度为0,发送一个值会阻塞住。

这就相当于快递员直接找张三,但是张三没了,但是快递员还得一直等着,等等等,然后挂了,终究还是没送出去。

有缓冲管道

 

 

这个就简单啦,多了一个快递柜,快递员直接将快递仍快递柜就行了。

示例代码

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "sync" 
  6.  
  7. var wg sync.WaitGroup 
  8.  
  9. //快递员,快递员放10个快递 
  10. func 快递员(kuaidigui chan string) { 
  11.     defer wg.Done() 
  12.     for i := 0; i < 10; i++ { 
  13.         fmt.Println("快递员放入了第",i,"快递"
  14.         kuaidigui <- fmt.Sprintf("第%d个快递", i) 
  15.     //放完快递就关闭了通道 
  16.     close(kuaidigui) 
  17.  
  18. //张三,拿走3个快递 
  19. func 张三(kuaidigui chan string) { 
  20.     defer wg.Done() 
  21.     for i := 0; i < 3; i++ { 
  22.         fmt.Println("张三拿走" + <-kuaidigui) 
  23. //李四拿走7个快递 
  24. func 李四(kuaidigui chan string) { 
  25.     defer wg.Done() 
  26.     for i := 0; i < 7; i++ { 
  27.         fmt.Println("李四拿走" + <-kuaidigui) 
  28. func main() { 
  29.     //快递柜,10个大小 
  30.     var 快递柜 = make(chan string, 10) 
  31.     wg.Add(3) 
  32.     go 快递员(快递柜) 
  33.     go 张三(快递柜) 
  34.     go 李四(快递柜) 
  35.     wg.Wait() 

执行结果 

 

遍历channel两种方式

代码

  1. func main() { 
  2.     //快递柜,10个大小 
  3.     var ch = make(chan int, 10) 
  4.     //向管道中发送值 
  5.     for i := 0; i < 10; i++ { 
  6.         ch <- i 
  7.     //方式一取值 
  8.     //for { 
  9.     //i, ok := <-ch 
  10.     ////取完值ok就是false 
  11.     //if !ok { 
  12.     //      //结束循环 
  13.     //      break 
  14.     //} 
  15.     //fmt.Println(i) 
  16.     //} 
  17.     //方式二取值 
  18.     for i:=range ch{ 
  19.         fmt.Println(i) 

执行结果


报错是因为我在main中完成了发送值和取值两个操作,所以会出现上述问题,但是结果是没有错的。

单向通道

我们知道通道是可以发送值和取值的,但是某些场景为了安全起见,理论来说只能取值,后者只能发送值。

单向通道通常只在函数参数中体现。

  • 形参 chan<- chan类型只写。
  • 形参 <-chan chan类型只读。

修改上述快递员代码。

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "sync" 
  6.  
  7. var wg sync.WaitGroup 
  8.  
  9. //快递员,快递员放10个快递,只写 chan<- string 
  10. func 快递员(kuaidigui chan<- string) { 
  11.     defer wg.Done() 
  12.     for i := 0; i < 10; i++ { 
  13.         fmt.Println("快递员放入了第", i, "快递"
  14.         kuaidigui <- fmt.Sprintf("第%d个快递", i) 
  15.     //放完快递就关闭了通道 
  16.     close(kuaidigui) 
  17.  
  18. //张三,拿走3个快递,只读<-chan string 
  19. func 张三(kuaidigui <-chan string) { 
  20.     defer wg.Done() 
  21.     for i := 0; i < 3; i++ { 
  22.         fmt.Println("张三拿走" + <-kuaidigui) 
  23.  
  24. //李四拿走7个快递 
  25. func 李四(kuaidigui <-chan string) { 
  26.     defer wg.Done() 
  27.     for i := 0; i < 7; i++ { 
  28.         fmt.Println("李四拿走" + <-kuaidigui) 
  29. func main() { 
  30.     //快递柜,10个大小 
  31.     var 快递柜 = make(chan string, 10) 
  32.     wg.Add(3) 
  33.     go 快递员(快递柜) 
  34.     go 张三(快递柜) 
  35.     go 李四(快递柜) 
  36.     wg.Wait() 

总结

上述讲述了Go语言并发如何和channel配合使用,毕竟我们一般的任务都不是单独运行的,都是互相配合的。

我们讲述了如何创建channel,如何使用channel,有缓冲管道和无缓冲管道区别,并且拒了一个快递员例子来展示协程和channel如何配合,最后用单向通道又加固了一下代码。

我的代码中使用了中文命名变量名是为了好看,实际开发中千万不要这样!!!

上述代码一定要敲一下,如果在操作过程中有任何问题,记得下面留言,我们看到会第一时间解决问题。

不积跬步无以至千里,不积小流无以成江海,给自己一个成长的时间

 

责任编辑:姜华 来源: Go语言进阶学习
相关推荐

2020-12-23 08:39:11

Go语言基础技术

2022-02-16 10:03:06

对象接口代码

2020-11-05 09:58:16

Go语言Map

2020-10-22 08:33:22

Go语言

2020-11-11 10:52:54

Go语言C语言

2021-10-09 07:10:31

Go语言基础

2020-12-30 09:04:32

Go语言TCPUDP

2022-04-27 10:01:43

切片Go封装

2020-10-25 07:33:13

Go语言

2020-12-09 09:59:32

Go语言技术

2020-10-23 08:38:19

Go语言

2020-12-07 05:59:02

语言Go接口

2021-10-30 10:43:04

语言Go函数

2021-11-03 10:02:07

Go基础函数

2021-09-29 10:00:07

Go语言基础

2021-10-13 10:00:52

Go语言基础

2020-10-22 11:15:47

Go语言变量

2021-10-16 10:17:51

Go语言数据类型

2021-02-20 10:06:14

语言文件操作

2021-01-13 08:40:04

Go语言文件操作
点赞
收藏

51CTO技术栈公众号