golang中channel使用

1 golang中channel使用

1.1 channel介绍

Go并发是源自CSP模型,通过channel来实现协程的同步,Go并发不通过共享内存来通信,而是通过通信来共享内存,Go内建channel实现了go协程之间数据的读写相关操作,通道(channel)是一种特殊的类型,在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。channel可以看作一个消息队列,遵循先进先出的原则,从而保证了收发数据的顺序性。

channel有如下特点:

  • channel本身是一个队列,先进先出
  • 线程安全,不需要枷锁,但是如果传递数组指针或者其他非线程安全的指针或引用,需要额外做好保护
  • channel是引用类型,必须make之后才能使用,一旦初始化容量,就不会改变了
  • 当写满时,不可以写,取空时,不可以取,如果容量满则发送阻塞直到通道数据被取走,如果通道数据为空,则接收阻塞,可通过select方式非阻塞读写
  • 通道一次只能接收一个数据元素
  • 每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型,比如chan int

Go语言提倡使用channel的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个channel,并提供了确保同步交换数据的机制,声明channel时,需要指定将要被共享的数据的类型,可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针等。

channel与goroutine交互示意图如下:
chan

1.2 channel使用

1.2.1 channel声明和初始化

golang中channel声明方式如下:
var 通道变量 chan 通道类型

例如:var chan1 chan int //通道传递int类型的数据

通过此声明chan1为nil,需要配合make进行初始化后,才能使用

var chan1 chan int
chan1 = make(chan int) //无缓冲
chan2 := make(chan int,3) //缓冲大小为3的通道
type as struct{
    data []byte
    name string
}
a := &as{
    make([]byte,0,1024),"buf"
}

chan3 := make(chan *as,3) //缓冲大小为3,传输类型为as的指针
a <- a  

1.2.2 channel数据发送与接收

通道创建好后,根据make时是否创建缓冲分为有缓冲chan和无缓冲的chan

1.2.2.1 无缓冲chan的数据传输

如果创建时缓冲大小设置为0,或者未设置,此时把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞,如果接收时一直未有数据发送,则也一直阻塞,实例如下:

package main

import "fmt"

func main() {
    chan1 := make(chan int)
    go gorun1(chan1)
    var a int = 3
    fmt.Println(a)
    chan1 <- a
    a++
    fmt.Println(a)
    chan1 <- a
    a++
    fmt.Println(a)
}

//传递的时chan引用
func gorun1(c chan int) {
    v := <-c
    fmt.Println("gorun1 receive:", v)
}
/*打印结果
3
4
gorun1 receive: 3
*/

gorun1是在go程中运行,第一次发送数据3时,gorun1读取了数据3并打印,第二次发送数据4时,由于gorun1已经退出,无人接收,所以一直阻塞在chan1<-a
无缓冲通道的chan比较常用于等待go程退出的场景,实例如下:

package main

import "fmt"

var done chan any

func main() {
 done = make(chan any)
 chan1 := make(chan int)
 go gorun1(chan1)
 go gorun2(chan1)
 a := <-done //阻塞,等待gorun1退出时执行close(done)
 fmt.Println(a)
}

func gorun2(c chan int) {
 c <- 3
 c <- 4
 c <- 5
 c <- 0
}

//传递的时chan引用
func gorun1(c chan int) {
 defer close(done) //close chan时会触发向chan发送一个nil
 for {
  select {
  case v := <-c:
   fmt.Println("gorun1 receive:", v)
   if v == 0 {
    return
   }
  }
 }
}

从例子中可以看出,直到gorun2向chan1发送0时,gorun1退出,整个进程才推出,实例还使用了select,实现了无缓冲非阻塞接收。这里done实际上实现了wait-set的功能。

chan数据接收时,可通过如下方式判断是否关闭,当无缓冲时,其阻塞,直到关闭时ok为false,v为nil

v,ok := <-chan1 //经过测试无缓冲时,无数据写入时,其是阻塞的,直到close或者写入数据时,才会被触发

数据接收时,还可使用for range方式循环遍历接收,实例如下:

package main

import (
 "fmt"
 "time"
)

var done chan any

func main() {
 done = make(chan any)
 chan1 := make(chan int)
 go gorun1(chan1)
 go gorun2(chan1)
 a, ok := <-done //等待gorun1退出
 fmt.Println(a, ok)
}

func gorun2(c chan int) {
 c <- 3
 time.Sleep(time.Duration(2) * time.Second)
 c <- 4
 time.Sleep(time.Duration(2) * time.Second)
 c <- 5
 time.Sleep(time.Duration(2) * time.Second)
 c <- 0
 time.Sleep(time.Duration(2) * time.Second)
}

//传递的时chan引用
func gorun1(c chan int) {
 defer close(done)  //close chan时会触发向chan发送一个nil
 for v := range c { //循环接收,如果无数据则阻塞直到数据到来
  fmt.Println("gorun1 receive:", v)
  if v == 0 {
   return
  }
 }
}
1.2.2.2 有缓冲chan数据传输

为了满足并发数据处理的需求,chan可以声明为带有缓冲区的通道,此时只有通道未满,数据都可写入缓冲器,接收端只要缓冲区不为空都可以接收数据,但是chan不支持批量读写,每次只能写入一个数据,每次只能读取一个数据,当缓冲区满时,发送端阻塞;当缓冲区为空时,接收端阻塞;可以通过select方式来避免阻塞,实例如下:

package main

import (
 "fmt"
)

func main() {
 chan1 := make(chan int, 3) //缓冲大小为3
 chan1 <- 1 //放入第一个数据
 fmt.Println(1)
 chan1 <- 2 //放入第二个数据
 fmt.Println(2)
 chan1 <- 3 //放入第三个数据
 fmt.Println(3)
 fmt.Println(<-chan1) //读取数据
 fmt.Println(<-chan1) //读取数据
 fmt.Println(<-chan1) //读取数据
 fmt.Println(<-chan1) //读取数据
}

实例声明了缓冲区大小为3的chan,在写入数据时,连续写入3个并未阻塞,连续接收三个也未阻塞,第四次读取时由于无数据,阻塞

有关阻塞/非阻塞/无缓冲/缓冲,可参照此篇文章:
https://blog.youkuaiyun.com/aggie4628/article/details/124646319

1.3 channel的应用场景

1.3.1 通过channel实现信号量功能

前面已经介绍了此种场景,通过无缓冲的chan,接收阻塞直到另一个并发go程处理完程,并发送close(chan)或者发送任意值后,触发当前现场继续执行,实例如下:

package main

import (
 "fmt"
 "time"
)

var done chan any

func main() {
 done = make(chan any)
 go task1()
 <-done//等待task1执行完成
 fmt.Println("task2")
}

func task1() {
 fmt.Println("task1")
 time.Sleep(time.Duration(5) * time.Second)
 done <- nil //执行完成后发送一个nil触发done解除阻塞
}

1.3.2 通过channel实现广播通知功能

可利用已关闭的channel读取数据时总是非阻塞的特性,可以实现在一个协程中向其他多个协程广播某个事件发生的通知,例如定义一个exitp chan any来控制所有go程退出,实例如下:

package main

import (
 "fmt"
 "time"
)

var exitp chan any

func main() {
 exitp = make(chan any)
 go task1()
 go task2()
 fmt.Println("task3")
 time.Sleep(time.Duration(3) * time.Second)
 close(exitp)
 time.Sleep(time.Duration(1) * time.Second)
}

func task1() {
 fmt.Println("task1")
 time.Sleep(time.Duration(1) * time.Second)
 <-exitp
 fmt.Println("exit task1")
}

func task2() {
 fmt.Println("task2")
 time.Sleep(time.Duration(1) * time.Second)
 <-exitp
 fmt.Println("exit task2")
}

通过close(chan)的特性,只能通知一次,不能携带参数,如果需要携带参数通知,则需要建立一个总线channel的方式,分发到子chan中,例如

package main

import (
 "fmt"
 "time"
)

type aa struct {
 message chan any
 name    string
}

func (p *aa) Run() {
 defer close(p.message)
 for {
  select {
  case ss := <-p.message:
   fmt.Println(p.name, ss)
  case <-Exitp:
   fmt.Println("exit :", p.name)
   return
  }
 }
}

var Eventbus chan any
var Exitp chan any
var Reicevermap map[string]*aa

func main() {
 Exitp = make(chan any) //控制go程退出
 Eventbus = make(chan any, 1)
 Reicevermap = make(map[string]*aa)
 a := &aa{make(chan any, 3), "aa"} //aa接收者
 go a.Run()                        //aa接收广播线程
 Reicevermap[a.name] = a
 b := &aa{make(chan any, 3), "bb"} //bb接收者
 go b.Run()                        //bb接收广播线程
 Reicevermap[b.name] = b
 fmt.Println("111")
 go broadcast()
 time.Sleep(time.Duration(1) * time.Second)
 Eventbus <- "第一个广播"
 time.Sleep(time.Duration(3) * time.Second)
 close(Exitp)
 time.Sleep(time.Duration(3) * time.Second)
}

func broadcast() {
 for event := range Eventbus {//不建议此种方式接收chan数据,建议用select
  for _, v := range Reicevermap {
   v.message <- event
  }
 }
}

1.3.3 通过channel互斥量

通过设置容量未1的chan来实现互斥的功能,这种用法较少,因为互斥量本来就可以用锁来实现,没必要滥用chan,这里就不做举例

1.3.4 channel多写多读控制

原则上尽量一个channel只有一个go程写,一个go程读取,这样最好控制,尽量避免多写多读的情况,此种情况如果无法避免,可以引入额外的chan来做分发,就如上述带参数的广播的例子,这里也不做举例

1.4 channel使用注意事项

  • channel不用时最好close关闭,虽然说gc会回收,但是还是有序关闭为好
  • channel不能重复close否则会报恐慌错误
  • 涉及到大数据传输时,传输指针或者引用,来避免大内存拷贝
  • channel灵活使用,可让多进程并发通信变得很简单,但是也不能滥用
<think>好的,我现在需要回答用户关于Golangchannel底层实现原理的问题。首先,我得回忆一下自己对channel的了解,并结合用户提供的引用内容来组织回答。用户提到了数据结构、源码解析,所以需要详细说明channel的结构体,比如hchan,以及各个字段的作用。 根据引用[3],通道在操作时需要判断是否初始化、是否关闭,还有处理阻塞的读写协程。这说明底层实现中使用了锁来保证并发安全,可能涉及互斥锁或者其他的同步机制。用户提供的引用中提到了sync.Map的结构,但channel的实现可能不同,需要区分开。 用户提供的引用[3]还详细描述了读写的流程,比如环形缓冲区、sendq和recvq队列。我需要解释这些数据结构的作用,比如当缓冲区满时,发送的goroutine会被阻塞并加入sendq队列,等待接收方取出数据后唤醒。这部分可以结合源码中的结构体字段,比如sendx和recvx指针,以及qcount表示元素数量。 另外,引用[5]提到当sendq为空且buffer为空时,接收的goroutine会被包装成sudog放入recvq队列。这说明在没有数据可读时,接收方会阻塞,直到有发送方发送数据。需要解释这种同步机制是如何实现的,可能涉及到goroutine的调度和等待队列的管理。 数据结构方面,hchan结构体应该包含元素类型、缓冲区相关的字段(如buf、sendx、recvx、qcount),以及两个等待队列sendq和recvq。锁(mutex)也是必须的,用于保护这些字段的并发访问。引用[3]中提到的lock加锁步骤验证了这一点。 源码解析部分,可能需要提到创建channel的函数makechan,发送和接收的底层函数chansend和chanrecv。根据引用[3],操作通道时需要先检查是否关闭,处理阻塞的协程,以及环形缓冲区的读写流程。例如,当缓冲区有空间时,直接将数据写入缓冲区,否则阻塞当前协程。 还需要区分无缓冲和有缓冲通道的不同处理方式。无缓冲的channel相当于缓冲区大小为1,发送和接收必须同时准备好才能完成数据传输,否则会被阻塞。引用[3]中提到,如果是无缓冲型,读取时会直接取出发送协程的元素,并唤醒该协程。 最后,用户可能关心channel的关闭机制和panic情况,比如向已关闭的channel发送数据会引发panic,而接收操作可以继续读取剩余数据。这部分需要结合源码中的closed字段,以及相关错误处理逻辑。 总结回答结构:首先介绍hchan的数据结构,然后分别说明发送、接收和关闭的流程,最后提及性能优化和注意事项。确保引用中的内容被正确标注,例如操作步骤中的锁和队列处理引用自[3]和[5]。</think>### Golang Channel 底层实现原理解析 #### 1. Channel 数据结构 Channel 的底层实现基于结构体 `hchan`,包含以下核心字段[^3][^5]: ```go type hchan struct { qcount uint // 当前缓冲区元素数量 dataqsiz uint // 缓冲区总容量 buf unsafe.Pointer // 环形缓冲区指针 elemsize uint16 // 元素大小 elemtype *_type // 元素类型 sendx uint // 缓冲区发送位置索引 recvx uint // 缓冲区接收位置索引 recvq waitq // 阻塞的接收协程队列(sudog链表) sendq waitq // 阻塞的发送协程队列(sudog链表) lock mutex // 互斥锁(保护并发操作) closed uint32 // 通道是否关闭(0-未关闭,1-已关闭) } ``` #### 2. 操作流程解析 ##### **发送操作(`ch <- x`)** 1. **初始化检查**:若通道未初始化(nil),发送会直接引发死锁。 2. **加锁**:通过 `lock` 字段保证操作原子性。 3. **关闭检查**:若通道已关闭,则触发 `panic`。 4. **处理阻塞接收协程**: - 若 `recvq` 队列非空,直接将数据拷贝到接收方,并唤醒对应协程[^3]。 5. **写入缓冲区**: - 若缓冲区未满,将数据写入 `buf` 的 `sendx` 位置,更新 `sendx` 和 `qcount`。 6. **阻塞等待**: - 若缓冲区已满,将当前协程包装为 `sudog` 加入 `sendq`,挂起并等待唤醒[^5]。 ##### **接收操作(`x := <-ch`)** 1. **初始化检查**:若通道未初始化,接收操作会引发死锁。 2. **加锁**:同样通过 `lock` 保证原子性。 3. **处理阻塞发送协程**: - 若 `sendq` 队列非空,直接从发送方获取数据(无缓冲型)或从缓冲区读取后补充发送数据(有缓冲型)。 4. **读取缓冲区**: - 若缓冲区非空,从 `recvx` 位置读取数据,更新 `recvx` 和 `qcount`。 5. **阻塞等待**: - 若缓冲区为空,将协程加入 `recvq` 并挂起[^5]。 ##### **关闭操作(`close(ch)`)** 1. **加锁**:确保关闭操作原子性。 2. **设置关闭标志**:标记 `closed = 1`。 3. **唤醒所有阻塞协程**:清空 `sendq` 和 `recvq` 队列,触发对应协程的 `panic` 或返回零值。 #### 3. 关键机制 - **环形缓冲区**:用于有缓冲通道,通过 `sendx` 和 `recvx` 实现循环写入/读取。 - **协程阻塞队列**:`sendq` 和 `recvq` 管理阻塞协程,唤醒时避免饥饿问题。 - **锁竞争优化**:通过 `lock` 保护核心字段,但频繁加锁可能影响性能,需合理设计缓冲区大小。 #### 4. 性能与注意事项 - **无缓冲通道**:本质是同步通信,依赖协程直接传递数据。 - **有缓冲通道**:异步通信,缓冲区满/空时才会阻塞。 - **死锁风险**:未初始化或未及时关闭的通道可能导致协程永久阻塞。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

艾浅觅

感谢您的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值