channal 实现数据结构及send方法

本文详细解析了Go语言中 Chan 数据结构的内部结构,包括其作为循环队列的工作方式,初始化方法makechan的细节,以及send和recv操作中的同步与阻塞策略。重点讲解了发送数据时的锁保护机制和接收者优先策略。
type hchan struct {
	qcount   uint           // total data in the queue
	dataqsiz uint           // size of the circular queue
	buf      unsafe.Pointer // points to an array of dataqsiz elements
	elemsize uint16
	closed   uint32
	elemtype *_type // element type
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters

	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
	//
	// Do not change another G's status while holding this lock
	// (in particular, do not ready a G), as this can deadlock
	// with stack shrinking.
	lock mutex
}

chan 类型的数据结构如图所示,本质上是一个循环队列,其中buf字段是存放元素的循环队列的 buffer,lock 则是保护字段的互斥锁。sendx和recvx 分别指向处理请求时数据的位置,sendq和recvq表示阻塞时等待队列。

方法

chan 有几个重要的方法,分别是初始化 make, 接受数据recv, 发送数据send 和关闭close,

初始化方法

func makechan(t *chantype, size int) *hchan {
	elem := t.elem

	// compiler checks this but be safe.
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}

	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}

	// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
	// buf points into the same allocation, elemtype is persistent.
	// SudoG's are referenced from their owning thread so they can't be collected.
	// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
	var c *hchan
	switch {
	case mem == 0:
		// Queue or element size is zero.
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.ptrdata == 0:
		// Elements do not contain pointers.
		// Allocate hchan and buf in one call.
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		// Elements contain pointers.
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}

	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
	}
	return c
}

初始化方法主要时根据类型和size 给hchan结构体分配空间和赋值

Send方法

Go 会把 send 语句转换成 chansend1 函数,chansend1 函数会调用 chansend

首先send 方法会检测chan是否为空,若为空,则阻塞

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
}

接下来检测chan是否已满或是否关闭,但是在调用chansend时,block被设置为true,所以不会触发chan已满返回false代码,chan关闭时,发送数据会panic,其中有加锁的逻辑

	if !block && c.closed == 0 && full(c) {
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)

	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

若发现此时有接收者等待,则直接将数据交给接收者

if sg := c.recvq.dequeue(); sg != nil {
		// Found a waiting receiver. We pass the value we want to send
		// directly to the receiver, bypassing the channel buffer (if any).
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

buf未满,将数据加入buf

if c.qcount < c.dataqsiz {
		// Space is available in the channel buffer. Enqueue the element to send.
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}

若buf已满,则阻塞

<think>好的,我现在要回答用户关于Kotlin Channel中send和trySend方法的区别的问题。首先,我需要回顾一下自己对Kotlin协程和Channel的理解。Channel是用于协程之间通信的一种结构,类似于队列,允许在不同协程之间发送和接收数据。 用户的问题集中在send和trySend的区别。根据之前的知识,我记得send是一个挂起函数,当Channel已满或者无法立即发送数据时,它会挂起当前协程,直到有空间可用。而trySend是非挂起函数,它会尝试立即发送数据,如果无法发送则返回失败,而不会阻塞或挂起协程。但需要更详细地确认这些细节。 现在,我需要查阅相关的引用资料。用户提供的引用中有几个涉及到Channel的行为。比如引用[2]提到Rendezvous Channel是没有缓冲区的,发送和接收必须同时准备好,否则会挂起。这说明send在缓冲区满时确实会挂起。而引用[3]中的retryWhen函数可能与错误处理有关,但暂时不太相关。引用[5]提到适配器的不同方法,但也不直接相关。 接下来,我需要确认trySend的行为。根据Kotlin官方文档,trySend是尝试立即将元素发送到Channel,并返回一个结果对象,指示是否成功。如果Channel已关闭,会返回失败。如果是非缓冲的Rendezvous Channel,trySend只有在有接收者等待时才会成功。否则,立即返回失败,而不会挂起。 另外,需要考虑Channel的不同类型,比如有缓冲和无缓冲的情况。例如,当使用有缓冲的Channel时,如果缓冲区未满,trySend会成功;如果已满,则失败。而send在缓冲区满时会挂起,直到有空间。 还需要提到Channel的状态对这两个方法的影响。比如,如果Channel已经被关闭,无论是send还是trySend都会失败。但send在关闭时会抛出异常,而trySend可能通过返回的结果对象来表明失败原因。 总结起来,两者的主要区别在于阻塞行为、返回值类型以及适用场景。send适用于需要等待的情况,而trySend适合非阻塞的即时尝试。此外,异常处理的方式也不同,send会抛出异常,而trySend通过结果对象处理。 现在,我需要将这些信息结构化,用中文清晰表达,并遵循用户给定的格式要求,如行内公式用$...$,引用相关文献。同时,生成相关问题。 最后,检查是否有遗漏点,比如Channel的不同类型对这两个方法的影响,或者具体的代码示例是否有必要。但用户没有要求代码示例,所以可能不需要。确保引用正确,例如引用[2]关于Rendezvous Channel的行为适用于send的情况。</think>### Kotlin Channel 中 `send` 与 `trySend` 的区别 在 Kotlin 协程的 `Channel` 中,`send` 和 `trySend` 是两种发送数据的方法,但它们在行为和应用场景上有显著差异: #### 1. **阻塞行为** - **`send`** 是挂起函数(`suspend` 修饰),当 `Channel` 的缓冲区已满(或为无缓冲的 Rendezvous Channel)时,调用 `send` 的协程会被挂起,直到接收端准备好处理数据。例如: ```kotlin channel.send(1) // 若缓冲区满,协程挂起 ``` 这种行为确保了数据发送的可靠性,但可能引入协程调度的延迟[^2]。 - **`trySend`** 是非挂起函数,它会立即尝试发送数据。若 `Channel` 无法立即接收(如缓冲区满或无接收者等待),则返回 `ChannelResult` 表示状态: ```kotlin val result = channel.trySend(1) if (result.isSuccess) { /* 发送成功 */ } ``` 此方法适用于非阻塞场景,例如在 UI 线程或需要快速响应的逻辑中。 #### 2. **返回值与异常处理** - **`send`** 在 `Channel` 关闭时会抛出 `ClosedSendChannelException`,需要通过协程的异常处理机制(如 `try/catch`)捕获。 - **`trySend`** 返回 `ChannelResult` 对象,包含是否成功(`isSuccess`)、失败原因(`isFailure`)或 `Channel` 是否关闭(`isClosed`)。这避免了异常抛出,更适合在不确定 `Channel` 状态的场景中使用。 #### 3. **适用场景** - **`send`**:适用于需要确保数据一定被发送的场景,允许协程等待直到条件满足。例如生产者-消费者模型中,生产者需保证数据交付。 - **`trySend`**:适用于非阻塞操作或需要快速失败(fail-fast)的场景。例如实时系统中,若发送失败需立即执行备用逻辑。 #### 4. **性能与调度** - **`send`** 的挂起可能触发协程调度器的重新调度,增加上下文切换的开销。 - **`trySend`** 无挂起操作,性能开销更低,但需手动处理失败情况。 ### 示例对比 ```kotlin // 使用 send(挂起) launch { channel.send(data) // 若缓冲区满,协程挂起 } // 使用 trySend(非阻塞) val result = channel.trySend(data) if (!result.isSuccess) { // 处理发送失败逻辑 } ``` ### 总结 | 特性 | `send` | `trySend` | |---------------|-------------------------|--------------------------| | **阻塞行为** | 挂起协程直到发送成功 | 立即返回结果,不挂起 | | **异常处理** | 抛出异常 | 返回结果对象 | | **适用场景** | 需保证数据必达 | 非阻塞或快速失败逻辑 |
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值