对于Go语言的并发,前面有几篇文章都有涉及到,如下:
Go语言并发比较二叉树(Binary Tree)
https://blog.csdn.net/weixin_41896770/article/details/127569147
Go语言进阶,闭包、指针、并发
https://blog.csdn.net/weixin_41896770/article/details/127547900但都只是一个示例,没有具体涉及到其中存在的一些问题,比如说很容易出现死锁等情况,这里重点讲解如何使用通道以及遇到的问题的处理方法。
先看个简单的创建通道,然后发送内容的代码:
- package main
-
- import "fmt"
-
- func main() {
- ch := make(chan string)
- ch <- "你好,寅恪光潜"
- fmt.Println(<-ch)
- }
看起来是没有问题,而且语法都是合规的,但是运行时会报错:
fatal error: all goroutines are asleep - deadlock!
这是比较常见的错误,也就是发生了死锁,信息显示goroutine没有启动,是睡眠的状态,当然这里没有使用到goroutine机制,那么换个角度来理解,这个通道虽然是申请了,由于是无缓冲通道,所以如果没有接收者的话,你发送到通道就会直接流出去(看成是水管),这样就没啥意义。
那么可以定义为有长度的通道,这样的话就相当于创建一种有缓冲机制的通道,让通道可以暂存数据,如下:
- func main() {
- ch := make(chan string, 1)//这里使其成为有缓冲的通道
- ch <- "你好,寅恪光潜"
- fmt.Println(<-ch)
- }
- //你好,寅恪光潜
使用goroutine机制,创建一个接收通道去接收发送通道发送过来的值,如下:
- func receiver(ch chan string) {
- r := <-ch
- fmt.Println("这里就接收到了发送来的值:", r)
- }
-
- func main() {
- ch := make(chan string)
- go receiver(ch)
- ch <- "你好,寅恪光潜"
- fmt.Println("发送成功")
- }
- //这里就接收到了发送来的值: 你好,寅恪光潜
- //发送成功
如果是多个goroutine并发的话将如何处理?我们将内容先全部发送到通道,然后再读取出来:
- package main
-
- import (
- "fmt"
- "time"
- )
-
- func f(ch chan<- string, i int) {
- time.Sleep(time.Duration(i) * time.Second)
- s := fmt.Sprintf("寅恪光潜%d", i)
- ch <- s
- }
-
- func main() {
- ch := make(chan string, 10)
- fmt.Println(ch, len(ch))
- for i := 0; i < 4; i++ {
- go f(ch, i)
- }
- for ret := range ch {
- fmt.Println(ret)
- }
- }
看起来好像是没有问题,启动goroutine发送内容到通道,然后遍历读取出通道的内容。不过在遍历完之后会报错:
0xc000062180 0
寅恪光潜0
寅恪光潜1
寅恪光潜2
寅恪光潜3
fatal error: all goroutines are asleep - deadlock!
错误原因是,读取完之后通道是空的,然后再读取(接收)的时候就会发生阻塞死锁。
这个时候我们使用sync.WaitGroup等待组,这个等待组的作用可以保证在并发环境中完成指定数量的任务。里面有几个方法的用法说明如下:
Add(1) 计数器+1 ; Done() 计数器-1或者 Add(-1)也可以 ;Wait() 等待计数器一直减到0,比如上面是循环4次,4个任务,我们将修改为:
- package main
-
- import (
- "fmt"
- "sync"
- "time"
- )
-
- var wg sync.WaitGroup
-
- func f(ch chan<- string, i int) {
- time.Sleep(time.Duration(i) * time.Second)
- s := fmt.Sprintf("寅恪光潜%d", i)
- ch <- s
- defer wg.Done() //完成一个任务,计数器就减1
- }
-
- func main() {
- ch := make(chan string, 10)
- go func() {
- wg.Wait() //一直等待直到完成
- close(ch) //记得关闭通道
- }()
- for i := 0; i < 4; i++ {
- wg.Add(1) //每个任务开始,计数器就就加1
- go f(ch, i)
- }
- for ret := range ch {
- fmt.Println(ret)
- }
- }
-
- /*
- 寅恪光潜0
- 寅恪光潜1
- 寅恪光潜2
- 寅恪光潜3
- */
除了上面的等待组外,还有一种常用的方法,使用select来同步并发,select会一直等待,直到某个case的通信操作完成时,执行case分支对应的语句,这个看起来更简洁一点,如下:
- func f(ch chan<- string, i int) {
- time.Sleep(time.Duration(i) * time.Second)
- s := fmt.Sprintf("寅恪光潜%d", i)
- ch <- s
- }
-
- func main() {
- ch := make(chan string, 10)
- for i := 0; i < 4; i++ {
- go f(ch, i)
- }
- for {
- select {
- case info := <-ch:
- println(info)
- default:
- time.Sleep(time.Second)
- fmt.Println("无数据")
- }
- }
- }
- /*
- 无数据
- 寅恪光潜0
- 寅恪光潜1
- 无数据
- 无数据
- 寅恪光潜2
- 寅恪光潜3
- 无数据
- 无数据
- ...
- */
然后会一直这样下去,这种情况是没有关闭通道,一直循环着,比如做聊天室这种一直监听的情况。
上面介绍是双向通道,可以发送也可以接收,当然你也可以定义只接收或者只发送的单通道。
定义接收通道
type Receiver <-chan int
定义发送通道
type Sender chan<- int
对于这个符号在左边还是右边,我个人是这样去理解记忆,<-chan表示从通道流出来,那就是需要拿东西接着,就是接收通道;chan<-表示指向通道,那就是往通道发送内容,就是发送通道。
- var ch = make(chan int, 3) //带缓冲的通道
- var sender Sender = ch //发送通道
- var receiver Receiver = ch //接收通道
看个示例:
- package main
-
- import (
- "fmt"
- "time"
- )
-
- type Sender chan<- int //发送通道
- type Receiver <-chan int //接收通道
-
- func main() {
- var ch = make(chan int, 0) //无缓冲的通道
- var num int = 110
-
- go func() {
- var sender Sender = ch
- sender <- num //往发送通道发送数据
- fmt.Println("发送的数据:", num)
- }()
-
- go func() {
- var receiver Receiver = ch //接收通道里的数据
- fmt.Println("接收的数据:", <-receiver)
- }()
- //让main函数执行结束的时间延迟1秒
- time.Sleep(time.Second)
- }
- /*
- 接收的数据: 110
- 发送的数据: 110
- */