Java/Go双修 - Go-Channel与Java的内存通信区别

0 阅读6分钟

Channel的理念

Channel非常重要,在Golang中有个重要思想:不以共享内存通信,而以通信来共享内存

Javaer应该能理解这句话是什么意思,在Java中我们无法在多线程之间通过直接传递的方式来共享数据

而是以一个比如静态变量这么个东西在一个类中,多个线程去修改这个静态变量的方式来进行通信共享数据

Channel是什么

Channel是一个可以收发数据的管道,简单来说就是可以在协程间传输数据的一个管道

channel的声明方式:

var channel_name chan channel_type
var channel_name [size]chan channel_tyepe

声明之后的管道,并没有进行初始化为其分配空间,其值是nil,我们要使用的话需要配合make函数进行初始化

var channel_name chan channel_type
channel_name = make(chan channel_type)
channel_name = make(chan channel_type, size)

也可以一步完成

channel_name := make(chan channel_type)
channel_name := make(chan channel_type, size) // 创建带有缓存的管道 size为缓存大小 下文会讲缓冲和非缓冲管道

Channel能做什么

Channel是一个可以收发数据的管道,简单来说就是可以在协程间传输数据的一个管道

ch := make(chan int)
go func() {
    ch <- 1 // 发送操作会阻塞,直到有接收者
    fmt.Println("发送成功")
}()
num := <-ch // 接收操作会阻塞,直到有发送者
  • 怎么读取Channel的数据
func main() {
	 ch := make(chan int, 5)
	 ch <- 1
	 ch <- 2
	 close(ch)
	 go func() {
	 		for value := range ch {
	 				fmt.Printf("value = %d\n",value)
	 		}
	 }()
	 time.Sleep(2 * time.Second)
}

Channel - 有缓冲和无缓冲

  • 无缓冲Channel (同步Channel)

    发送和接收操作必须同时进行,当一个协程尝试向无缓冲Channel发送数据时

    若没有其他协程准备好接收,发送操作会阻塞当前协程,直到有接收者,接收操作也是如此

    ch := make(chan int)
    go func() {
        ch <- 1 // 发送操作会阻塞,直到有接收者
        fmt.Println("发送成功")
    }()
    num := <-ch // 接收操作会阻塞,直到有发送者
    
  • 有缓冲Channel (异步Channel)

    允许发送和接收操作在一定程度上异步进行,当缓冲区未满的时候,发送操作不会阻塞

    当缓冲区满的时候,发送操作会阻塞,直到有接收者从Channel取出数据

    当Channel未关闭的时候,缓冲区为空的时候,接收操作会阻塞,直到有数据被发送到Channel

    当Channel关闭了的时候,接收操作会立即返回,并得到该Channel元素类型的零值

    ch := make(chan int, 3) // 创建一个容量为3的有缓冲Channel
    ch <- 1 // 不会阻塞
    ch <- 2 // 不会阻塞
    ch <- 3 // 不会阻塞
    // ch <- 4 // 此时会阻塞,因为缓冲区已满 直到有接收操作取出缓冲区的数据
    

Channel - 单向和双向 (不重要)

Channel根据其功能又可以分为双向Channel和单向Channel,双向Channel既可以发送数据又可以接收数据

单向Channel要么只能发送数据,要么只能接收数据,注意双向Channel不是意味着Channel的两端能出能进

  • 定义单向读Channel

    var ch = make(chan int)
    type RChannel= <-chan int  // 定义类型
    var rec RChannel = ch
    
  • 定义单向写Channel

    var ch = make(chan int)
    type SChannel = chan<- int // 定义类型
    var send SChannel = ch
    
  • 代码示例

    import (
        "fmt"
        "time"
    )
    
    type SChannel = chan<- int
    type RChannel = <-chan int
    
    func main() {
        var ch = make(chan int)  // 创建channel
    
        go func() {
            var send SChannel = ch
            fmt.Println("send: 100")
            send <- 100
        }()
    
        go func() {
            var rec RChannel = ch
            num := <- rec
            fmt.Printf("receive: %d", num)
        }()
        time.Sleep(2*time.Second)
    }
    

一般我们都不会进行单向Channel的使用,知道就行

Channel的底层实现原理 (定义及数据结构)

Channel的底层定义,Channel用make函数创建初始化的时候会在堆上分配一个runtime.hchan类型的数据结构

type hchan struct {
	qcount   uint           // 循环队列的元素总数
	dataqsiz uint           // 循环队列的大小
	buf      unsafe.Pointer // 指向循环队列的指针
	elemsize uint16				  // 循环队列中的每个元素的大小
	closed   uint32					// 标记位 标记Channel是否关闭
	elemtype *_type 				// 循环队列中的元素类型
	sendx    uint   				// 已发送元素在循环队列中的索引位置
	recvx    uint   				// 已接收元素在循环队列中的索引位置
	recvq    waitq  				// 等待向Channel接收消息的sudog队列
	sendq    waitq  				// 等待向Channel写入消息的sudog队列
	lock mutex							// 互斥锁,对Channel的数据读写操作进行加锁,保证并发安全
}

可以看到Channel的底层实现是有锁的,是通过Mutex来保证线程安全的,也就是说Channel是一个线程安全的组件

Channel的底层数据结构

image.png

Channel的读写操作

  • 写操作
  1. 加锁保护,lock(&c.lock) 对hchan结构体加互斥锁,确保操作原子性(避免并发修改缓冲区或等待队列)
  2. 查看接收队列recvq是否存在sudog在等待数据如果有的话,直接将数据发给队首的接收协程 (跳过缓冲区),将数据从发送协程复制到接收协程的存储地址,唤醒接收协程,解锁并返回
  3. 处理有缓冲Channel:
    1. 若缓冲区未满 (c.qcount < c.dataqsiz):将数据放入缓冲区中sendx指向的位置,并对sendx进行移位 (sendx + 1) % dataqsiz,同时更新c.qucount(元素数量 + 1),解锁并返回
    2. 若缓冲区已满 (c.qcount == c.dataqsiz):将当前发送协程加入发送队列 (sendq),标记位阻塞状态,解锁并返回
  4. 处理无缓冲Channel:
    1. 无缓冲区,数据必须直接发送给接收协程,将当前发送协程加入发送队列 (sendq),阻塞等待接收者出现,解锁并返回
  • 读操作
  1. 加锁保护,lock(&c.lock) 对hchan结构体加互斥锁,确保操作原子性(避免并发修改缓冲区或等待队列)
  2. 查看发送队列sendq是否存在sudog在发送数据,如果有的话,直接从发送协程获取数据 (跳过缓冲区),将数据从发送协程复制到接收协程的存储地址,唤醒发送协程,发送协程的阻塞状态解除,可继续执行,解锁并返回
  3. 处理有缓冲Channel:
    1. 若缓冲区非空 (c.qcount > 0):从缓冲区中recvx只想的位置读取数据,更新c.qcount (元素数量 -1),recvx++环形递增,解锁并返回
    2. 若缓冲区为空 (c.qcount = 0):将当前接收协程加入接收队列recvq,标记位阻塞状态,解锁并返回
  4. 处理无缓冲Channel:
    1. 无缓冲区,必须等待发送协程发送数据,若发送队列为空,则进入接收队列(这一步检查是没有的,因为第二步已经检查过了)

关于Channel的总结

  • 关闭一个未初始化的Channel会产生Panic
  • Channel只能被关闭一次,对同一个Channel重复关闭会产生Panic
  • 向一个已经关闭了的Channel发送消息会产生Panic
  • 从一个已经关闭的Channel读取消息不会发生Panic,会一直读取数据(零值)
  • Channel是并发安全的,多个Goroutine同时读取Channel中的数据,不会产生并发安全问题