go进阶(2) -深入理解Channel实现原理

2023-02-27 16:19:47 浏览数 (1)

Go的并发模型已经在https://cloud.tencent.com/developer/article/2227925 详细说明。

Go的CSP并发模型,是通过goroutinechannel来实现的。

  • channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
  • Go并发的核心哲学是不要通过共享内存进行通信; 相反,通过沟通分享记忆。

      channel是Go提供goroutine间的通信方式,使用channel可以使多个goroutine之间通信。channel是进程内的通信方式,通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。

如需跨进程通信,Go建议用分布式系统的方法来解决,如使用Socket或者HTTP等通信协议,Go语言在网络方面也有非常完善的支持。

channel是类型相关的,一个channel只能传递一种类型的值,这个类型需要在声 明channel时指定。

 1、channel基本语法:

每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

声明通道:var 通道变量 chan 通道类型:var channame chan ElementType

创建通道:make(chan 数据类型, 缓冲大小):

var channame chan <- ElementType //只写

var channame <- chan ElementType //只读

chanName := make(chan int) //无缓存channel

chanName := make(chan in,0) //无缓存channel

chanName := make(chan int,100) //有缓存channel

channel跟map类似的在使用之前都需要使用make进行初始化

ch1 := make(chan int, 5)

未初始化的channel零值默认为nil

代码语言:javascript复制
var ch chan int
fmt.Println(ch) // <nil>

2、通信机制:

传数据用channel <- data,取数据用<-channel。例子:

代码语言:javascript复制
channel <- 1 //向channel添加一个值为1
<- channel //从channel取出一个值
a := <- channel //从channel取出一个值并赋值给a
a,b := <- channel //从channel取出一个值赋值给a,如果channel已经关闭或channel没有值,b为false
  • 成对出现:在通信过程中,传数据channel <- data**和取数据**<-channel**必然会成对出现**,因为这边传,那边取,两个goroutine之间才会实现通信。
  • 阻塞:不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
  • channel仅允许被一个goroutine读写。

1)同步,主协程和子协程之间通信:

代码语言:javascript复制
func main(){
    ch := make(chan int)
    go func() {
        ch <- 996 //向ch添加元素
    }()
    a := <- ch
    fmt.Println(a)
    fmt.Println("程序结束!")
}

2)、两个子协程的通信

使用channel实现两个goroutine之间通信。

代码语言:javascript复制
func two() {
    tc := make(chan string)
    ch := make(chan int)
    // 第一个协程
    go func() {
        tc <- "协程A,我在添加数据"
        ch <- 1
    }()
    // 第二个协程
    go func() {
        content := <- tc
        fmt.Printf("协程B,我在读取数据:%sn",content)
        ch <- 2
    }()
    <- ch
    <- ch
    fmt.Println("程序结素!")
}
func main(){
    two()
}

3)、channel仅允许被一个goroutine读写。

代码语言:javascript复制
package main
import (
    "fmt"
    "time"
)
func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b chan int) {
    val := <-b
    fmt.Println("goRoutineB  received the data", val)
}
func main() {
    ch := make(chan int, 3)
    go goRoutineA(ch)
    go goRoutineB(ch)
    ch <- 3
    time.Sleep(time.Second * 1)
}

3、channel缓冲区

无缓冲通道,make(chan int),指在接收前没有能力保存任何值的通道,这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。

有缓冲通道,make(chan int, 2),指在被接收前能存储一个或者多个值的通道,这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。

 例子:

代码语言:javascript复制
package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch1 <- 5
	rec := <-ch1
	fmt.Println("ch1被接受,程序结束:rec:,", rec)
}
//fatal error: all goroutines are asleep - deadlock!          

由于ch1没有缓冲区,channel没有缓冲区的话:

只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。

如果想要运行成功那么在发送信息前就应该有另外的协程等待着接收

代码语言:javascript复制
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	go receive(ch1)
	ch1 <- 5
	time.Sleep(time.Second)

}
func receive(ch1 chan int) {
	for {
		select {
		case rec2 := <-ch1:
			fmt.Println("ch1被接受,程序结束:rec:,", rec2)
		}
	}
}
//ch1被接受,程序结束:rec:, 5  

 但是如果有缓冲区就能避免程序阻塞,可以将发送的channel放在缓冲区直至有接收方将它接收

向channel添加数据超过缓存,会出现死锁:

代码语言:javascript复制
func main() {
	ch := make(chan int,3)
	ch <- 1
	//<- ch
	ch <- 1
	ch <- 1
	ch <- 1
	fmt.Println("ok")
}

4、 单向通道

<- chan int // 只接收通道,只能接收不能发送

chan <- int // 只发送通道,只能发送不能接收

而对于close方法只能是发送通道拥有

5、select与channel配合使用:使用Select来进行调度

Select 和 swith结构很像,但是select中的case的条件只能是I/O。

Select 的使用方式类似于 switch 语句,它也有一系列 case 分支和一个默认的分支。 每个 case分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case分支对应的语句。

具体格式如下:

代码语言:javascript复制
select {
case <-ch1:
	//...
case rec := <-ch2:
	//...
case ch3 <- 10:
	//...
default:
	//默认操作
}

select里面case是随机执行的,如果都不满足条件,那么就执行default

select总结:

  • 每个case必须是一个I/O操作
  • case是随机执行的:如果多个 case 同时满足,select 会随机选择一个执行。
  • 如果所有case不能执行,那么会执行default
  • 如果所有case不能执行,且没有default,会出现阻塞
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出

实现一个一直接收消息:

代码语言:javascript复制
func main() {
	ch := make(chan int)
	for i := 1; i <= 10; i   {
		go func(j int) {
			ch <- j
		}(i)
	}
	for {
		select {
		case a1 := <- ch:
			fmt.Println(a1)
		default:
		}
	}
}

0 人点赞