前言

在之前的文章中,我们已经知道了协程的启动、挂起、取消、异常以及常用的协程作用域等基础应用。
这些基础应用适合的场景是一次性任务,执行完就结束了的场景。

launch / async 适合的场景

  • 网络请求
  • 数据库查询
  • 文件读写
  • 并行计算任务
  • 等等

Channel 适合的场景

而对于一些相对复杂的场景,例如:持续的数据流、需要在不同的协程之间传递数据、需要顺序或背压控制等场景,基础的 launch / async
就不够用了。

例如:

  • 用户点击、输入等事件流的处理
  • 生产者-消费者模型的需求:任务排队、日志流
  • 高频数据源处理(相机帧、音频流等)

类似这种持续的、需要顺序控制、或者多个协程配合执行的场景,就需要用到 Channel 了。


Channel 的概念和基本使用

概念

顾名思义,Channel 有管道、通道的意思。Channel 跟 Java 中的 BlockingQueue 很相似,区别在于 Channel 是挂起的,不是阻塞的。

Channel 的核心特点就是能够在不同的协程之间进行数据传递,并且能够控制数据传递的顺序。
使用起来很简单,基本就分为以下几步:

  1. 创建 Channel
  2. 通过 channel.send 发送数据
  3. 通过 channel.receive 接收数据

整体的概念也比较简单形象,就是一根管道,一个口子发送数据,一个口子接收数据。

Channel 的创建

先来看下 Channel 的源码,可以看到会根据传入的参数选择不同的实现。

public fun <E> Channel(capacity: Int = RENDEZVOUS,onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E> =when (capacity) {RENDEZVOUS -> {if (onBufferOverflow == BufferOverflow.SUSPEND)BufferedChannel(RENDEZVOUS, onUndeliveredElement) // an efficient implementation of rendezvous channelelseConflatedBufferedChannel(1,onBufferOverflow,onUndeliveredElement) // support buffer overflow with buffered channel}CONFLATED -> {require(onBufferOverflow == BufferOverflow.SUSPEND) {"CONFLATED capacity cannot be used with non-default onBufferOverflow"}ConflatedBufferedChannel(1, BufferOverflow.DROP_OLDEST, onUndeliveredElement)}UNLIMITED -> BufferedChannel(UNLIMITED,onUndeliveredElement) // ignores onBufferOverflow: it has buffer, but it never overflowsBUFFERED -> { // uses default capacity with SUSPENDif (onBufferOverflow == BufferOverflow.SUSPEND) BufferedChannel(CHANNEL_DEFAULT_CAPACITY,onUndeliveredElement)else ConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)}else -> {if (onBufferOverflow === BufferOverflow.SUSPEND) BufferedChannel(capacity, onUndeliveredElement)else ConflatedBufferedChannel(capacity, onBufferOverflow, onUndeliveredElement)}}

参数概览

参数类型默认值描述
capacityIntRENDEZVOUS通道容量,决定缓冲区大小和行为模式
onBufferOverflowBufferOverflowSUSPEND缓冲区溢出时的处理策略
onUndeliveredElement((E) -> Unit)?null元素未能送达时的回调函数
capacity(容量配置)

capacity 参数决定了 Channel 的缓冲行为和容量大小:

  • RENDEZVOUS(值为 0):无缓冲,发送者和接收者必须同时准备好
  • CONFLATED(值为 -1):只保留最新的元素,旧元素会被覆盖
  • UNLIMITED(值为 Int.MAX_VALUE):理论上就是无限容量,永不阻塞发送
  • BUFFERED(值为 64):默认缓冲大小
  • 自定义正整数:自己指定具体的缓冲区大小
onBufferOverflow(溢出策略)

当缓冲区满时的处理策略:

  • SUSPEND:挂起发送操作,等待缓冲区有空间(默认)
  • DROP_OLDEST:丢弃旧的元素,添加新元素
  • DROP_LATEST:丢弃新元素,保留缓冲区中的现有元素
onUndeliveredElement(未送达回调)

当元素无法送达时的清理回调函数:

  • null:不执行任何清理操作(默认)
  • 自定义函数:用于资源清理、日志记录等,根据业务需求来定义

参数组合效果

capacityonBufferOverflow行为适用场景
RENDEZVOUSSUSPEND无缓冲,同步通信严格的生产者-消费者同步
BUFFEREDSUSPEND有限缓冲,满时挂起一般的异步处理,默认的缓冲数量是 64
UNLIMITEDSUSPEND缓冲长度为 Int.MAX_VALUE高吞吐量场景(生产上不建议使用,有内存方面的风险)
CONFLATEDDROP_OLDEST无缓冲,只保留最新值状态更新、实时数据
自定义大小SUSPEND固定大小,满时挂起批量处理、批量任务
自定义大小DROP_OLDEST固定大小,丢弃旧数据获取最近 N 个元素
自定义大小DROP_LATEST固定大小,拒绝新数据保护重要历史数据

Capacity

RENDEZVOUS(会合模式)

特点:

  • 容量为 0,无缓冲区
  • 发送者和接收者必须同时准备好才能完成数据传输
  • 提供强同步保证,一手交钱一手交货

使用示例:

suspend fun demonstrateRendezvousChannel() {// 创建 RENDEZVOUS Channel(默认容量为 0),默认什么都不传就是 rendezvous 模式,Channel<String>()val rendezvousChannel = Channel<String>(Channel.RENDEZVOUS)// 启动发送者协程val senderJob = GlobalScope.launch {println("[发送者] 准备发送消息...")rendezvousChannel.send("Hello from RENDEZVOUS!")println("[发送者] 消息已发送")rendezvousChannel.send("Second message")println("[发送者] 第二条消息已发送")rendezvousChannel.close()}// 启动接收者协程val receiverJob = GlobalScope.launch {delay(1000) // 延迟1秒,发送者会等待接收者准备好println("[接收者] 开始接收消息...")for (message in rendezvousChannel) {println("[接收者] 收到消息: $message")delay(500) // 模拟处理时间}println("[接收者] Channel已关闭")}// 等待所有协程完成joinAll(senderJob, receiverJob)
}

执行结果
在这里插入图片描述

CONFLATED(只留最新值)

特点:

  • 容量为 1,但会丢弃旧值
  • 只保留最新的元素
  • 发送操作永不阻塞
  • 只能使用 BufferOverflow.SUSPEND 策略

源码分析:

CONFLATED -> {require(onBufferOverflow == BufferOverflow.SUSPEND) {"CONFLATED capacity cannot be used with non-default onBufferOverflow"}ConflatedBufferedChannel(1, BufferOverflow.DROP_OLDEST, onUndeliveredElement)
}

使用示例:

suspend fun demonstrateConflatedChannel() {// 创建 CONFLATED Channel,相当于:Channel<String>(1, BufferOverflow.DROP_OLDEST)val conflatedChannel = Channel<String>(Channel.CONFLATED)// 快速发送多个消息val senderJob = GlobalScope.launch {repeat(5) { i ->val message = "Update-$i"conflatedChannel.send(message)println("[发送者] 发送更新: $message")delay(100) // 短暂延迟}conflatedChannel.close()}// 慢速接收者val receiverJob = GlobalScope.launch {delay(1000) // 延迟1秒,让发送者发送完所有消息println("[接收者] 开始接收(只会收到最新的值)...")for (message in conflatedChannel) {println("[接收者] 收到: $message")}}joinAll(senderJob, receiverJob)
}

在这里插入图片描述

UNLIMITED(无限容量)

特点:

  • 容量为 Int.MAX_VALUE,理论上无限容量
  • 发送操作永不阻塞,但要注意内存使用
  • 忽略 onBufferOverflow 参数
  • 适用于高吞吐量场景,但生产环境需谨慎使用
suspend fun demonstrateUnlimitedChannel() {val unlimitedChannel = Channel<String>(Channel.UNLIMITED)val senderJob = GlobalScope.launch {repeat(10) { i ->val message = "Message-$i"unlimitedChannel.send(message)println("[发送者] 立即发送: $message")}unlimitedChannel.close()println("[发送者] 所有消息已发送,Channel已关闭")}val receiverJob = GlobalScope.launch {delay(1000) // 延迟1秒开始接收println("[接收者] 开始慢速接收...")for (message in unlimitedChannel) {println("[接收者] 处理: $message")delay(300) // 模拟处理时间}}joinAll(senderJob, receiverJob)
}

在这里插入图片描述

BUFFERED(有限容量)

特点:

  • 使用默认容量 (CHANNEL_DEFAULT_CAPACITY,通常为 64)
  • 在缓冲区满时根据 onBufferOverflow 策略处理

源码分析:

BUFFERED -> {if (onBufferOverflow == BufferOverflow.SUSPEND)BufferedChannel(CHANNEL_DEFAULT_CAPACITY, onUndeliveredElement)elseConflatedBufferedChannel(1, onBufferOverflow, onUndeliveredElement)
}

使用示例:

suspend fun demonstrateBufferedDefaultChannel() {// 创建 BUFFERED Channel(默认容量为 64)val bufferedChannel = Channel<String>(Channel.BUFFERED)val senderJob = GlobalScope.launch {repeat(100) { i ->bufferedChannel.send("Message-$i")println("[发送者] 发送 Message-$i")}bufferedChannel.close()}val receiverJob = GlobalScope.launch {delay(1000) // 延迟接收for (message in bufferedChannel) {println("[接收者] 收到: $message")delay(50)}}joinAll(senderJob, receiverJob)
}

与下面自定义容量效果类似。

自定义容量

特点:

  • 指定具体的缓冲区大小
  • 根据 onBufferOverflow 策略处理溢出

源码分析:

else -> {if (onBufferOverflow === BufferOverflow.SUSPEND)BufferedChannel(capacity, onUndeliveredElement)elseConflatedBufferedChannel(capacity, onBufferOverflow, onUndeliveredElement)
}

使用示例:

suspend fun demonstrateBufferedChannel() {// 创建容量为3的缓冲Channelval bufferedChannel = Channel<Int>(capacity = 3)// 启动发送者协程val senderJob = GlobalScope.launch {repeat(5) { i ->println("[发送者] 发送数字: $i")bufferedChannel.send(i)println("[发送者] 数字 $i 已发送")}bufferedChannel.close()println("[发送者] Channel已关闭")}// 启动接收者协程,延迟接收以观察缓冲效果val receiverJob = GlobalScope.launch {delay(2000) // 延迟2秒开始接收println("[接收者] 开始接收数字...")for (number in bufferedChannel) {println("[接收者] 收到数字: $number")delay(800) // 模拟慢速处理}}joinAll(senderJob, receiverJob)
}

可以看到,因为默认的溢出策略是 SUSPEND,所以当缓冲区满了时,发送者会被挂起,直到接收者处理完一个元素,才会继续发送。
在这里插入图片描述


BufferOverflow 策略详解

当 Channel 的缓冲区满时,BufferOverflow 参数决定了如何处理新的发送请求:

SUSPEND(默认策略)

  • 行为:当缓冲区满时,挂起发送操作直到有空间可用
  • 特点:提供背压控制,防止生产者过快
  • 使用场景:需要确保所有数据都被处理的场景
suspend fun demonstrateBasicOperations() {//容量为 2,溢出策略为SUSPENDval channel = Channel<String>(capacity = 2, onBufferOverflow = BufferOverflow.SUSPEND)//发送的速度快val job1 = GlobalScope.launch {repeat(5) {channel.send("Message-$it")println("[发送者] 发送 Message-$it")}channel.close()}val job2 = GlobalScope.launch {//除了用 channel.recrive 外,也可以直接 用 for 循环接收数据for (message in channel) {//接收的速度慢delay(1000)println("[接收者] 接收到: $message")}}joinAll(job1, job2)
}

在这里插入图片描述

DROP_LATEST

  • 行为:当缓冲区满时,丢弃新元素,保留缓冲区中的现有元素
  • 特点:保护已缓冲的数据不被覆盖
  • 使用场景:保护重要的历史数据,防止新数据覆盖
  • 性能特点:发送操作永不阻塞,但新数据可能被丢弃
suspend fun demonstrateBasicOperations() {val channel = Channel<String>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_LATEST)val job1 = GlobalScope.launch {repeat(5) {channel.send("Message-$it")println("[发送者] 发送 Message-$it")}channel.close()}val job2 = GlobalScope.launch {for (message in channel) {delay(1000)println("[接收者] 接收到: $message")}}joinAll(job1, job2)
}

可以看到,当缓冲区满时,会把新数据丢弃掉,因此,接收端只接收到了旧数据。

在这里插入图片描述

DROP_OLDEST

  • 行为:当缓冲区满时,丢弃旧的元素,添加新元素
  • 特点:保持固定的内存使用,优先保留新数据
  • 使用场景:实时数据流、最近N个元素
  • 性能特点:发送操作永不阻塞,但可能丢失历史数据
suspend fun demonstrateBasicOperations() {val channel = Channel<String>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)val job1 = GlobalScope.launch {repeat(5) {channel.send("Message-$it")println("[发送者] 发送 Message-$it")}channel.close()}val job2 = GlobalScope.launch {for (message in channel) {delay(1000)println("[接收者] 接收到: $message")}}joinAll(job1, job2)
}

在这里插入图片描述

需要注意的是,当缓冲区满了之后,1 和 2 被丢弃了,3 和 4 被放进去了。从这里可以看出,丢弃数据时,并不是把最早的旧数据丢掉,这里跟内部的实现有关。


onUndeliveredElement 回调

当元素无法送达时(如 Channel 被取消或关闭),会调用此回调函数

suspend fun demonstrateBasicOperations() {val channel = Channel<String>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST) {println("[Channel] 缓冲区已满,无法放到缓冲区,值:${it}")}// 演示基本的send和receive操作val job1 = GlobalScope.launch {repeat(5) {channel.send("Message-$it")println("[发送者] 发送 Message-$it")}channel.close()}val job2 = GlobalScope.launch {for (message in channel) {delay(1000)println("[接收者] 接收到: $message")}}joinAll(job1, job2)
}

在这里插入图片描述


Channel 操作方式

Channel 提供了两种操作方式:阻塞操作和非阻塞操作。

阻塞操作(send/receive)

send()receive() 方法都是挂起方法,它们会阻塞当前协程,直到完成操作。

非阻塞操作(trySend/tryReceive)

trySend()tryReceive() 是 Channel 提供的非阻塞操作 API。与阻塞版本不同,这些方法会立即返回结果,不会挂起当前协程,也不会抛出异常。

操作对比

操作类型阻塞版本非阻塞版本行为差异
发送send()trySend()send() 会挂起直到有空间;trySend() 立即返回结果
接收receive()tryReceive()receive() 会挂起直到有数据;tryReceive() 立即返回结果

返回值类型

  • trySend() 返回 ChannelResult<Unit>
  • tryReceive() 返回 ChannelResult<T>

ChannelResult 是一个密封类,通过密封类中的成员 isSuccessgetOrNull() 可以判断操作是否成功。

在这里插入图片描述

大部分场景下,send / receive + 合理的 Channel 配置就能解决问题,trySend/tryReceive 更多的是想达到如下效果:

  • 避免不必要的协程挂起开销,希望立即得到结果
  • 提供更精细的控制逻辑,如:超时处理、重试机制等
  • 实现更好的错误处理和用户反馈,能更好地处理异常场景

runBlocking {val channel = Channel<Int>(2)val sendJob = launch {repeat(5) {delay(100)val sendResult = channel.trySend(it)sendResult.onSuccess {println("发送成功")}.onFailure {println("发送失败")}.onClosed {println("通道已关闭")}}}val receiveJob = launch {for (i in channel) {delay(300)println("接收到数据:${i}")}}joinAll(sendJob, receiveJob)}

在这里插入图片描述

Channel 状态管理

Channel 在其生命周期中会经历以下几个关键状态:

  • 活跃状态(Active):可以正常发送和接收数据
  • 发送端关闭(Closed for Send):不能发送新数据,但可以接收缓冲区中的数据
  • 接收端关闭(Closed for Receive):不能接收数据,缓冲区已清空
  • 取消状态(Cancelled):Channel 被取消,所有操作都会失败

API

  • channel.close():关闭 Channel
  • channel.isClosedForSend:判断发送端是否已关闭
  • channel.isClosedForReceive:判断接收端是否已关闭
  • channel.cancel():取消 Channel

Close(关闭操作)

  • 调用 close() 后,isClosedForSend 立即变为 true
  • 此时,缓冲区中的数据仍可被消费
  • 只有当缓冲区清空后,isClosedForReceive 才变为 true

示例:

    suspend fun demonstrateChannelClose() {val channel = Channel<String>(1)val producer = GlobalScope.launch {try {for (i in 1..5) {val message = "Message $i"println("准备发送: $message")channel.send(message)println("成功发送: $message")delay(100)}} catch (e: ClosedSendChannelException) {println("生产者: Channel已关闭,无法发送数据 - ${e.message}")}}val consumer = GlobalScope.launch {try {for (message in channel) {println("接收到: $message")delay(200)}println("消费者: Channel已关闭,退出接收循环")} catch (e: Exception) {println("消费者异常: ${e.message}")}}delay(300) // 模拟让一些数据能够被接收到// 检查Channel状态println("关闭前状态:")println("  isClosedForSend: ${channel.isClosedForSend}")println("  isClosedForReceive: ${channel.isClosedForReceive}")// 关闭Channelprintln("\n正在关闭Channel...")channel.close()// 检查关闭后的状态println("关闭后状态:")println("  isClosedForSend: ${channel.isClosedForSend}")println("  isClosedForReceive: ${channel.isClosedForReceive}")// 等待协程完成producer.join()consumer.join()println("最终状态:")println("  isClosedForSend: ${channel.isClosedForSend}")println("  isClosedForReceive: ${channel.isClosedForReceive}")
}

在这里插入图片描述

Cancel(取消操作)

cancel() 方法用于强制取消 Channel,它会:

  • 立即关闭发送和接收端
  • 清空缓冲区中的所有数据
  • 触发 onUndeliveredElement 回调(如果设置了)
suspend fun demonstrateChannelCancel() {val channel = Channel<String>(capacity = 5) {println("消息未被接收:${it}")}val producer = GlobalScope.launch {try {for (i in 1..8) {val message = "Message $i"println("尝试发送: $message")channel.send(message)println("成功发送: $message")delay(100)}} catch (e: CancellationException) {println("生产者: Channel被取消 - ${e.message}")}}val consumer = GlobalScope.launch {try {for (message in channel) {println("接收到: $message")delay(300)}} catch (e: CancellationException) {println("消费者: 协程被取消 - ${e.message}")}}delay(400) // 让一些操作执行println("\n取消前状态:")println("  isClosedForSend: ${channel.isClosedForSend}")println("  isClosedForReceive: ${channel.isClosedForReceive}")// 取消Channelprintln("\n正在取消Channel...")channel.cancel(CancellationException("主动取消Channel"))println("取消后状态:")println("  isClosedForSend: ${channel.isClosedForSend}")println("  isClosedForReceive: ${channel.isClosedForReceive}")// 等待协程完成producer.join()consumer.join()
}

在这里插入图片描述


Channel 异常处理

在使用 Channel 的过程中,会遇到各种异常情况。主要包括以下几种类型:

ClosedSendChannelException

触发条件:

  • 在已关闭的 Channel 上调用 send() 方法
  • Channel 调用 close() 后,发送端立即关闭

示例:

suspend fun demonstrateClosedSendException() {val channel = Channel<String>()// 关闭 Channelchannel.close()try {// 尝试在已关闭的 Channel 上发送数据channel.send("This will throw exception")} catch (e: ClosedSendChannelException) {println("捕获异常: ${e.message}")println("异常类型: ${e::class.simpleName}")}
}

ClosedReceiveChannelException

触发条件:

  • 从已关闭且缓冲区为空的 Channel 调用 receive() 方法
  • isClosedForReceivetrue 时调用 receive()

示例:

suspend fun demonstrateClosedReceiveException() {val channel = Channel<String>()// 关闭 Channelchannel.close()try {// 尝试从已关闭且空的 Channel 接收数据val message = channel.receive()println("收到消息: $message")} catch (e: ClosedReceiveChannelException) {println("捕获异常: ${e.message}")println("异常类型: ${e::class.simpleName}")}
}

CancellationException

触发条件:

  • Channel 被 cancel() 方法取消
  • 父协程被取消,导致 Channel 操作被取消
  • 超时或其他取消信号

示例:

suspend fun demonstrateCancellationException() {val channel = Channel<String>()val job = GlobalScope.launch {try {// 这个操作会被取消channel.send("This will be cancelled")} catch (e: CancellationException) {println("发送操作被取消: ${e.message}")throw e // 重新抛出 CancellationException}}delay(100)// 取消 Channelchannel.cancel(CancellationException("手动取消 Channel"))try {job.join()} catch (e: CancellationException) {println("协程被取消: ${e.message}")}
}

异常与状态关系

Channel 状态send() 行为receive() 行为trySend() 行为tryReceive() 行为
活跃状态正常发送或挂起正常接收或挂起返回成功/失败结果返回成功/失败结果
发送端关闭抛出 ClosedSendChannelException正常接收缓冲区数据返回失败结果正常返回结果
接收端关闭抛出 ClosedSendChannelException抛出 ClosedReceiveChannelException返回失败结果返回失败结果
已取消抛出 CancellationException抛出 CancellationException返回失败结果返回失败结果

异常处理技巧

使用非阻塞操作避免异常

非阻塞操作不会抛出异常,而是返回结果对象:

suspend fun safeChannelOperations() {val channel = Channel<String>()// 安全的发送操作val sendResult = channel.trySend("Safe message")when {sendResult.isSuccess -> println("发送成功")sendResult.isFailure -> println("发送失败: ${sendResult.exceptionOrNull()}")sendResult.isClosed -> println("Channel 已关闭")}// 安全的接收操作val receiveResult = channel.tryReceive()when {receiveResult.isSuccess -> println("接收到: ${receiveResult.getOrNull()}")receiveResult.isFailure -> println("接收失败: ${receiveResult.exceptionOrNull()}")receiveResult.isClosed -> println("Channel 已关闭")}
}
健壮的异常处理
suspend fun robustChannelUsage() {val channel = Channel<String>()val producer = GlobalScope.launch {try {repeat(5) { i ->if (channel.isClosedForSend) {println("Channel 已关闭,停止发送")break}channel.send("Message $i")delay(100)}} catch (e: ClosedSendChannelException) {println("生产者: Channel 已关闭")} catch (e: CancellationException) {println("生产者: 操作被取消")throw e // 重新抛出取消异常} finally {println("生产者: 清理资源")}}val consumer = GlobalScope.launch {try {while (!channel.isClosedForReceive) {try {val message = channel.receive()println("消费者: 收到 $message")} catch (e: ClosedReceiveChannelException) {println("消费者: Channel 已关闭且无更多数据")break}delay(200)}} catch (e: CancellationException) {println("消费者: 操作被取消")throw e} finally {println("消费者: 清理资源")}}delay(1000)channel.close()joinAll(producer, consumer)
}

总结

Channel 关键概念对比

特性RENDEZVOUSCONFLATEDBUFFEREDUNLIMITED自定义容量
容量0164Int.MAX_VALUE指定值
缓冲行为无缓冲,同步只保留最新值有限缓冲无限缓冲有限缓冲
发送阻塞缓冲满时缓冲满时
适用场景严格同步状态更新一般异步高吞吐量批量处理
内存风险中等可控

溢出策略对比

策略行为性能特点适用场景
SUSPEND挂起发送操作提供背压控制确保数据完整性
DROP_OLDEST丢弃旧元素发送不阻塞实时数据流
DROP_LATEST丢弃新元素发送不阻塞保护历史数据

操作方式

操作类型阻塞版本非阻塞版本异常处理返回值
发送send()trySend()抛出异常ChannelResult<Unit>
接收receive()tryReceive()抛出异常ChannelResult<T>
特点会挂起协程立即返回需要 try-catch通过结果对象判断

Channel 状态生命周期

状态描述send()receive()检查方法
活跃正常工作状态✅ 正常✅ 正常-
发送关闭调用 close() 后❌ 异常✅ 可接收缓冲区数据isClosedForSend
接收关闭缓冲区清空后❌ 异常❌ 异常isClosedForReceive
已取消调用 cancel() 后❌ 异常❌ 异常-

总体来说,Channel 是一种非常强大的协程通信机制,它可以帮助我们在协程之间进行安全、高效的通信。在使用 Channel时,我们需要注意异常处理、缓冲区容量、溢出策略等问题。


感谢阅读,如果对你有帮助请三连(点赞、收藏、加关注)支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919379.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919379.shtml
英文地址,请注明出处:http://en.pswp.cn/news/919379.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux系统装google chrome,amd64

google chrome官网最下边其他平台&#xff0c;linux 查看自己的系统架构&#xff08;用下边这行代码查看&#xff09;&#xff0c;看看是amd还是 &#xff0c;我的显示amd64&#xff0c;amd对应.deb,rpm对应x86 &#xff0c;选择下载 dpkg --print-architecture 然后 sudo…

【C++基础】C++ 中const与volatile关键字深度解析:从面试考点到底层实现

在 C 开发岗位的面试中&#xff0c;const与volatile关键字是高频考点之一。这两个关键字看似简单&#xff0c;但实际上蕴含着丰富的语义和底层机制。本文从基础语法到高级应用&#xff0c;结合大厂真题&#xff0c;深入解析这两个关键字的奥秘。一、const关键字&#xff1a;常量…

达梦分布式集群DPC_故障分析_yxy

达梦分布式集群DPC_节点故障分析1 DPC核心概念回顾2 场景1-主库故障3 场景2-少数备库故障4 场景3-多数节点故障4.1 多数节点故障&#xff08;包括主库&#xff09;4.2 多数备库节点故障&#xff08;不包括主库&#xff09;1 DPC核心概念回顾 达梦分布式集群DPC&#xff0c;基于…

【高并发内存池】一、简介 定长内存池实现

文章目录Ⅰ. 项目介绍1、这个项目要做什么2、项目的要求Ⅱ. 什么是内存池1、池化技术2、内存池3、mallocⅢ. 设计一个定长内存池1、定长内存池的概念2、实现如何实现定长❓❓❓如何绕开 malloc 向堆直接申请空间❓❓❓3、性能测试Ⅰ. 项目介绍 1、这个项目要做什么 tcmalloc源…

产品设计.原型设计

产品思维&#xff1a; 1. 产品定位&#xff1a;产品的具体的、用户画像&#xff1b; --什么样的人在什么环境下做什么事情的场景 2. 范围层: 发现、识别和决策需求。--识别真假需求&#xff0c;做ROI判断 3. 可复用的、MVP产品方案--要能复用的解决方案&#xff0c;最小可用产品…

vue3+element-plus 输入框el-input设置背景颜色和字体颜色,样式效果等同于不可编辑的效果

应用效果&#xff1a;代码&#xff1a;<template> ......<el-form-item label"文件编号" label-position"right"><el-input v-model"qualityFileForm.fileNo" clearable :disabled"!props.isNew" /></el-form-it…

[ CSS 前端 ] 网页内容的修饰

目录 一. CSS 1. 概述 2. 基本语法 (1)行内样式表 (2)内嵌样式表 (3)外部样式表 3. 选择器 (1)标签选择器: (2)类选择器: (3)通配选择器: (4)后代选择器: 4. 基础样式 (1). 文本样式 (2). 背景样式 (3). 列表样式 5. 伪类 (1)定义: (2)伪类的语法&#xff1a; …

全面深入了解榛树游戏引擎

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;榛树游戏引擎&#xff08;Hazel&#xff09;是一款专为游戏开发设计的先进软件工具&#xff0c;它集成了多种功能&#xff0c;支持现代图形API&#xff0c;具有高性能的物理模拟系统和易学易用的脚本语言&#…

“大模型”技术专栏 | 浅谈基于 Kubernetes 的 LLM 分布式推理框架架构:概览

编者按&#xff1a;人工智能正以前所未有的渗透力重塑生产与生活图景。作为国内领先的数据智能科技企业&#xff0c;和鲸科技自 2015 年成立以来&#xff0c;深耕人工智能与数据科学&#xff0c;历经十年发展&#xff0c;已在气象、教育、医疗、航空航天、金融、通信、能源、零…

【JS】认识并实现一个chrome扩展程序

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍chrome扩展程序。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&#…

jeecgboot项目遇见的一些问题:

1.当你想修改项目的标题&#xff0c;前端将jeecgboot改成你想要的标题的时候&#xff0c;去前端的.env文件中进行修改。图1 修改标题根据路径找到文件&#xff0c;将网站标题改成自己需要的就可以正常显示了。图2 显示前图3 显示后2.在动态数组中&#xff0c;如果你知道数组需要…

项目里程碑设定有哪些方法

要科学设定项目里程碑&#xff0c;可采用以下几种方法&#xff1a;基于项目阶段划分法、关键交付物导向法、依赖关系链分析法、时间驱动法、风险节点识别法、目标成果导向法、资源约束分析法、客户验收节点设定法。其中&#xff0c;关键交付物导向法尤为实用。该方法以项目中必…

英伟达显卡驱动怎么更新 详细步骤教程

英伟达显卡驱动程序对于电脑的图形性能至关重要&#xff0c;它能确保显卡在游戏、设计、视频渲染等方面发挥最大性能。如果驱动过旧&#xff0c;可能会导致游戏运行不畅、软件不兼容&#xff0c;甚至系统出现错误。因此&#xff0c;定期更新英伟达显卡驱动非常必要。下面将为大…

基于单片机智能拐杖/导盲杖/老人防摔倒设计

传送门 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目速选一览表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品题目功能速览 概述 该设计针对老年人及行动不便人群的需求&#xff0c;开发了一款集成单片机控制的智能拐杖。拐杖采…

Node.js完整安装配置指南(包含国内镜像配置)

Node.js完整安装配置指南&#xff08;包含国内镜像配置&#xff09; 一、Node.js安装 方法1&#xff1a;使用Chocolatey安装&#xff08;推荐&#xff09; # 安装最新LTS版本 choco install nodejs# 或安装指定版本 choco install nodejs --version20.11.0方法2&#xff1a;…

AI硬件 - AMD显卡架构演进及产品线

目录 一、AMD显卡架构演进总结 二、典型AMD AI显卡历代型号参数对比表 关键参数说明: 三、AMD 特供中国AI显卡产品线全览 1. 企业级Instinct系列(数据中心/科研) 2. 消费级AI加速显卡(开发/本地推理) 四、与NVIDIA显卡的AI性能对比 关键指标实测数据 五、模型框架…

论文阅读-Gated CRF Loss for Weakly Supervised Semantic Image Segmentation

文章目录1 背景2 模块2.1 部分交叉熵损失2.2 弱标签&#xff08;线/点&#xff09;2.3 Gated CRF Loss3 效果3.1 总体效果3.2 消融实验4 总结参考文献1 背景 全监督的语义分割需要对全图进行完全而精确的标注。当需要标注的目标在图像中较多&#xff0c;又或形状不规则&#x…

零墨云A4mini打印机设置电脑通过局域网络进行打印

文档时间&#xff1a;2025年8月 1.演示环境 操作系统版本&#xff1a;Windows11 打印机版本&#xff1a;零墨云A4mini 这款打印机打印的方式有蓝牙、远程云和局域网&#xff0c;这里演示的是电脑通过局域网打印 通过电脑版局域网(这个局域网是网络可达)打印之前&#xff0c…

ESP8266 入门(第 3 部分):使用 Arduino IDE 对 ESP8266 进行编程并刷新其内存

使用 Arduino IDE 对 ESP8266 进行编程并刷新其内存 这是我们之前 ESP 教程的延续的第三个教程,其中我们将学习使用 Arduino IDE(不使用 Arduino)对 ESP8266 进行编程和烧录 ESP8266。在前面的教程中,我们介绍了 WiFi 收发器ESP8266简介以及将 AT 命令与 ESP8266 结合使用。…

如何成功初始化一个模块

一、如何保证成功初始化一个模块&#xff08;以 UART 为例&#xff09;要成功初始化一个模块&#xff0c;请遵循以下步骤&#xff1a;在图形化界面中&#xff0c;首先配置外设模块。紧接着&#xff0c;配置使用到的外设模块的引脚&#xff08;这一点很重要&#xff0c;容易忘记…