golang之channel

版权声明:如有需要,可供转载,但请注明出处:https://blog.youkuaiyun.com/City_of_skey/article/details/86263168

目录

1、channel简介

2、channel阻塞

3、channel非阻塞

4、select


1、channel简介

go语言中并发程序非常简单直接用关键字go就可以起一个协程,go语言的协程是语言级别的所以它的开销比线程、进程要小,因为线程和进程是系统级别的并发。go语言协程同步可以通过channel来解决。

channel有三种类型:

var aa chan int        /*可读可写*/
var aa chan <- int     /*只能写*/
var aa <- chan int     /*只能读*/

channel类型必须先用make初始化再使用

aa := make(chan int)

关闭channel用函数close,一个关闭的channel还可以去读数据,但不能在写如数据,如果写入数据就会产生panic

aa := make(chan int)
close(aa)
aa <- 4    /*panic*/

 

 

2、channel阻塞

对于buffer为1的channel,从channel中读数据时当没有读到就会阻塞,向channel中写数据如果没有空间存储也会阻塞。

读阻塞:

aa := make(chan int)

b := <- aa    /*aa中没有数据就会阻塞*/

写阻塞:

aa := make(chan int)

aa <- 4
aa <- 5    /*4还没有取出来,channel中没有空间, 阻塞*/

3、channel非阻塞

当我们需要非阻塞的channel我们可以给channel分配空间,当空间没有填满channel不会阻塞,有buffer的channel相当于一个队列,遵循先进先出的规则。

aa := make(chan int, 4)

带buffer的channel读取可以用range。但要注意的是当channel中的数据被取完后就会阻塞,如果没有再往channel中写数据就会产生死锁

	w := make(chan int, 4)

	w <- 4
	w <- 5

	for b := range w {
        /*最后一个5取走后就会阻塞,产生死锁*/
		fmt.Printf("w:%d\n", b)
	}

避免死锁方法:

每次range读取数据后判断channel的长度,如果是0就说明channel为空,就应该退出。

w := make(chan int, 4)

	w <- 1
	w <- 2
	w <- 3
	w <- 4

	for b := range w {
		fmt.Printf("w:%d\n", b)
        
        /*如果channel长度是0就退出循环,不然会阻塞产生死锁*/
		if len(w) <= 0 {
			break
		}

	}

4、select

go中的select相当于c语言中的switch,是对对个channle做选择,它不是循环,如果要循环判断就要在外面在加一个for

package main

import (
	"fmt"
	"time"
)

func main() {
	a := make(chan int)
	b := make(chan int)
	defer close(a)
	defer close(b)

	go func() {
		for i := 1; i < 3; i++ {
			a <- i
			b <- (i + 1)
			time.Sleep(1 * time.Second)
		}

	}()

	for {
		select {
		case val1 := <-a:
			fmt.Println("val1:", val1)
		case val2 := <-b:
			fmt.Println("val2:", val2)
		case <-time.After(3 * time.Second): /*3秒超时*/
			fmt.Println("time out quit")
			return
		}
	}

}

select中可以加一个超时时间,超过这个时间通道中还没有数据就直接返回。

<think>好的,我现在需要回答用户关于Golangchannel底层实现原理的问题。首先,我得回忆一下自己对channel的了解,并结合用户提供的引用内容来组织回答。用户提到了数据结构、源码解析,所以需要详细说明channel的结构体,比如hchan,以及各个字段的作用。 根据引用[3],通道在操作时需要判断是否初始化、是否关闭,还有处理阻塞的读写协程。这说明底层实现中使用了锁来保证并发安全,可能涉及互斥锁或者其他的同步机制。用户提供的引用中提到了sync.Map的结构,但channel的实现可能不同,需要区分开。 用户提供的引用[3]还详细描述了读写的流程,比如环形缓冲区、sendqrecvq队列。我需要解释这些数据结构的作用,比如当缓冲区满时,发送的goroutine会被阻塞并加入sendq队列,等待接收方取出数据后唤醒。这部分可以结合源码中的结构体字段,比如sendxrecvx指针,以及qcount表示元素数量。 另外,引用[5]提到当sendq为空且buffer为空时,接收的goroutine会被包装成sudog放入recvq队列。这说明在没有数据可读时,接收方会阻塞,直到有发送方发送数据。需要解释这种同步机制是如何实现的,可能涉及到goroutine的调度等待队列的管理。 数据结构方面,hchan结构体应该包含元素类型、缓冲区相关的字段(如buf、sendx、recvx、qcount),以及两个等待队列sendqrecvq。锁(mutex)也是必须的,用于保护这些字段的并发访问。引用[3]中提到的lock加锁步骤验证了这一点。 源码解析部分,可能需要提到创建channel的函数makechan,发送接收的底层函数chansendchanrecv。根据引用[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. 性能与注意事项 - **无缓冲通道**:本质是同步通信,依赖协程直接传递数据。 - **有缓冲通道**:异步通信,缓冲区满/空时才会阻塞。 - **死锁风险**:未初始化或未及时关闭的通道可能导致协程永久阻塞。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值