Go并发通信——Channel
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。(DO NOT COMMUNICATE BY SHARING MEMORY; INSTEAD, SHARE MEMORY BY COMMUNICATING.)
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
通道Channel
声明 | var 变量 chan 元素类型 | make(chan 元素类型, [缓冲大小]) —为引用类型声明内存地址 |
1. channel—注意点
- 向缓冲区已满的通道写入数据会导致goroutine阻塞
- 通道中没有数据,读取该通道会导致阻塞(阻塞即goroutine等待,等到缓冲区不满/有数据继续运行,如果主线程main进入等待,则会直接报死锁错误)
- 读取已关闭的通道不会引发阻塞,而是返回通道元素类型的零值
- 向已关闭的通道写数据会导致panic,重复关闭通道也会引发panic
2. channel—基础操作
ch <- 10 //10存入通道
x := <- ch //通道中取出第一个值赋给x
<- ch //取出值,忽略结果
close(ch) //关闭通道( 通道会被垃圾回收机制回收,不一定非要关闭 )
//**************************************************************************************
select{ //类似于switch,哪个条件满足就会执行哪个,多个满足则随机执行一个,没有满足则阻塞等待
case <-ch1:
...
case data := <-ch2:
...
case ch3<-data:
...
default:
默认操作
}
//select实现通知退出机制
func dosth(done chan struct{}) chan int {
ch := make(chan int)
go func() {
Label:
for {
select {
case ch <- rand.Int():
case <-done: 接收到done信号停止运行
break Label
}
}
close(ch)
}()
return ch
}
func main() {
done := make(chan struct{})
ch := dosth(done)
fmt.Println(<-ch)
fmt.Println(<-ch)
done <- struct{}{}
fmt.Println(<-ch)
}
//**************************************************************************************
for v := range in { //for range 进行通道读取,只有当in关闭时,才会终止循环
out <- v + 1
}
close(out)
3. channel—无缓冲通道 VS 有缓冲通道
(1)无缓冲通道
无缓冲通道 | ch := make(chan int) |
//对于无缓冲通道,必须有进程在等待接收参数,才可以向通道中传入参数。比如下面的方法,如果把a()的执行放在ch<-10后面,就会引发死锁错误。
func a(ch chan int) {
fmt.Printf("我是a,接收到了数字:%d\n", <-ch)
}
func main() {
ch := make(chan int)
go a(ch)
ch <- 10
fmt.Print("over")
}
//可以用来实现goroutine同步
func main() {
ch := make(chan struct{})
go func(){
time.Sleep(3*time.second)
ch<-struct{}
}()
<-ch
fmt.Print("over")
}
(2)有缓冲通道
有缓冲通道 | make(chan int, 1) //创建一个容量为1的有缓冲区通道 |
获取通道元素数量 | len(channel) |
获取通道容量 | cap(channel) |
(3)单向通道(在赋值或者传参时,我们将其设为单向通道,使得该函数只能对其进行读/写一个操作)
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
4. channel—用例
(1)实现管道
//一个函数的输入参数和输出参数是相同的chan类型,则函数可以调用自己,形成一个调用链
func chain(in chan int) chan int { //作用是将输入的通道值加一
out := make(chan int)
go func() {
for v := range in { //由于in,out声明的都是无缓冲,所以每读一个都会阻塞,等下一个参数
out <- v + 1
}
close(out)
}()
return out
}
func Guandao() {
in := make(chan int)
go func() {
for i := 0; i < 10; i++ { //初始化输入参数
in <- i
}
close(in)
}()
out := chain(chain(chain(in))) //叠加形成管道
for v := range out { //从最终的输出通道读出结果
fmt.Println(v)
}
}
(2)实现每个事务一个goroutine
//通过设置任务通道,每当有新任务来临时,为其分配相应的goroutine进行执行
type task struct {
num int
}
func (t *task) job(wait sync.WaitGroup) {
fmt.Println("i am", t.num, "WORKING")
time.Sleep(3 * time.Second) //模拟工作流程
fmt.Println("i am", t.num, "DONE")
wait.Done() //工作完成,wait-1
}
func AllocateJob() {
wait := sync.WaitGroup{} //wait防止程序未执行完成就退出
taskchan := make(chan *task, 10) //接收任务通道
go func() {
for i := 1; i <= 10; i++ { //模拟有新工作加入,1秒加入一个
task := &task{num: i}
wait.Add(1) //wait+1
time.Sleep(1 * time.Second)
taskchan <- task //任务加入任务通道
}
}()
go func() {
for task := range taskchan { //从通道中取出任务,并分配goroutine执行
go task.job(wait)
}
}()
wait.Wait()
}
(3)实现future模式
适用于一个流程中需要调用多个子调用的情况,并且这些子调用之间没有依赖。future模式的优势就是将其同步调用转化为异步调用。
//通过通道,实现在执行一个较为耗时的任务时,先通过goroutine调用该任务,由chan传入参数后,并行做其他的事,最后再通过通道获取其运行的结果。
type query struct {
in chan string
out chan string
}
func doquery(q query) {
in := <-q.in
time.Sleep(5 * time.Second) //模拟访问数据库进行查询
q.out <- in + "的结果"
}
func Futuretest() {
temp := query{in: make(chan string), out: make(chan string)}
go doquery(temp)
temp.in <- "select * from user"
time.Sleep(3 * time.Second) //模拟在进行数据库查询时,我们可以做一些其他的事情
fmt.Println(<-temp.out) //做完其他的事情,并且数据库查询也完成了,我们从通道中获取查询结果
}