go语言中channel的设计原理

目录

  • CSP模型
  • 使用channel
    • 创建通道
    • 发送数据到通道
    • 使用通道接收数据
    • 单向通道
    • 带缓冲通道
    • 通道的多路复用 select
  • channel底层原理
  • 参考


CSP模型

go中的channel类型可以在goroutine之间发送和接收消息。

传统的并发编程模式——多线程编程,大多时候是基于共享内存的方式来完成的。传统多线程的并发模式使用locks锁、condition variable条件变量 等同步原语或硬件指令来强制规定进程的推进顺序。
除了直接控制线程,还有CSP和Actor模型是基于消息传递。
这里我只简单介绍一下CSP。

CSP——Communicating Sequential Processes
CSP的核心思想是多个线程之间通过Channel来通信,类似于操作系统中的Pipe。CSP中channel是第一类对象,它不关注发送消息的实体。

go的channel正是借鉴了CSP这种模型。

这里再多说一些,

goroutine底层是使用协程coroutine实现并发,coroutine是一种运行在用户态的用户线程,有以下特点:

  • 用户空间避免了内核态和用户态的切换导致的成本
  • 可以由语言和框架层进行调度
  • 更小的栈空间允许创建大量的实例

不过学过操作系统的同学应该都知道,这样的并发最终还是会涉及到内核空间的。只是用户线程和内核线程的调度方案可以是一对一、多对一、多对多,使用多对多在这里可以达到提高效率的效果。


使用channel

创建通道

ch1 := make(chan int) //创建一个整型类型的通道
ch2 := make(chan interface{
     }) //可以存放任意类型

发送数据到通道

ch1 <- 0

使用通道接收数据

data := <-ch1
//执行该语句时将会阻塞,直到接收到数据并赋值给data变量

单向通道

ch := make(chan int)
var ch1 chan<- int = ch //只能发送

ch2 := make(<-chan int) //只能读

带缓冲通道

//创建一个3个元素缓冲大小的整型通道
ch := make(chan int, 3)

channel分为有缓冲和无缓冲的,无缓冲的只有当发送方和接收方都准备好才会传送数据,否则会被阻塞。在一个goroutine内同时进行读取和发送的话会导致死锁。
有缓冲的channel只有当缓冲区被装满了之后才会阻塞发送者,只有当缓冲区为空时才会阻塞接收者。

可以使用close关键字关闭一个channel,但是原则上由发送者关闭,如果仍然向一个已关闭的channel发送数据,会导致panic。关闭channel之后,接收方仍然可以从channel中读出数据默认值0,但是状态已经变了。

通道的多路复用 select

Go语言中的select 和操作系统中的系统调用select比较相似。

C语言的select系统调用可以同时监听多个文件描述符的可读或者可写的状态,Go 语言的select可以让Goroutine同时等待多个Channel可读或可写,在多个文件或Channel状态改变之前,select会一直阻塞当前线程或Goroutine。

select是与switch相似的控制结构,不过select的case中的表达式必须都是channel的收发操作。当select中的多个case同时被触发时,会随机执行其中一个。

通常情况下,select语言会阻塞goroutine并等待多个Channel中的一个达到可以收发的状态。但如果有default语句,可以实现非阻塞,就是当多个channel都不能执行的时候,运行default。

同时处理接收和发送多个通道的数据。

Go 语言中提供了select关键字,可以响应多个通道的操作。

select的每个case都会对应一个通道的收发过程。

当收发完成时,就会触发case中响应的语句。

select{
     
  case 操作1:
  	响应操作1
  case 操作2:
  ……
  default:
  ……
}

channel底层原理

type hchan struct {
     
    qcount   uint           // 通道中的元素个数
    dataqsiz uint           // 通道中循环队列的长度
    buf  unsafe.Pointer //缓冲链表的指针,缓冲链表是一个循环链表
    elemsize uint16  // 能接收发的元素大小
    closed   uint32
    elemtype *_type // 能接收发的元素类型

	//sendx和recvx用于记录buf这个循环链表中的发送或者接收的index
    sendx    uint   // 发送操作处理到的位置
    recvx    uint   // 接收操作处理到的位置

	//分别是接收或者发送的goroutine抽象出来的结构体(sudog)的队列,
	//都是双向链表
    recvq    waitq  // 接收队列
    sendq    waitq  // 发送队列
    lock mutex
}

创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针。使用过程中channel在函数之间的传递都是用的这个指针。

通道使用互斥锁mutex,让goroutine以先进先出FIFO的方式进入一个结构体中。

当需要发送或者接受的时候,要锁住hchan这个结构体。
缓存中按链表顺序存放,取数据按链表顺序读取。

当通道满了,再继续往通道发送会阻塞当前goroutine,原理是通过Go运行时的scheduler来完成调度。


参考

https://www.jianshu.com/p/36e246c6153d
https://blog.csdn.net/sixdaycoder/article/details/90751972
https://www.cnblogs.com/lianggx6/p/12558663.html
https://blog.csdn.net/guyan0319/article/details/90201405
《Go语言高级编程》

你可能感兴趣的:(go,golang)