对于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机制
使用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等待组
这个时候我们使用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来同步并发,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
*/