Coroutine 基础五 —— Flow 之 Channel 篇
1、Channel 与 Flow 简介与对比
所有知识都可总结为一个字 —— 流。包括数据流、事件流、状态流。
开发中最常用的 StateFlow 提供状态订阅。可以将一些信息包进 StateFlow 中进行保存。比如界面上显示的字符串,或者系统级别的信息,如用户状态。装进 StateFlow 中的状态就成为可订阅的状态,当状态值发生改变时通知所有订阅的位置,这样就能实现界面自动更新之类的自动化操作。
StateFlow 内部使用 SharedFlow 实现。SharedFlow 提供的是事件订阅而不是状态订阅。事件订阅和状态订阅的区别不多,但是比较关键。比如,在一个事件触发之后再进行事件订阅,这个事件原则上无需推送到订阅者一端(原则上无需推送,但你也可以配置成依然推送)。但状态订阅就不同,在状态更新之后发生的状态订阅,状态仍需要推送给订阅者。
StateFlow 内部使用 Flow 实现。Flow 不是订阅工具,而是数据流工具。SharedFlow 底层是事件流模型,而 Flow 准确地说并不是事件流,而是数据流。数据流与事件流并没有天差地别的不同,甚至可以从某个角度看成是一类东西。区分它们是为了应对不同的应用场景。
Channel 与 Flow 并不完全是一个体系的,但在实现上,它是 Flow 下层的一个关键支撑。Flow 的核心是数据流,而 Channel 是协程间协作的工具,提供在协程间传递数据的功能。它与 async 提供的功能有点像,只不过它可以多次发送数据让其他协程使用,而 async 是一次发送数据。如果数据流不需要跨协程,那么就应该使用 Flow 而不是 Channel,否则会遇到一些性能问题。并且,Channel 的 API 在数据流的角度也没有 Flow 的灵活和强大。
2、用 produce() 来提供跨协程的事件流
日常开发中几乎用不到 Channel,但是如果在基础架构团队负责给公司造轮子,或者担任架构师或技术研究员的位置,Channel 的知识是必要的。
Channel 是协程间通信的关键技术点,相对比较底层,但又没底层到对开发者完全透明的程度。想把 Flow 弄清楚,Channel 是绕不过去的东西。
Channel 相当于多条数据版的 async(),那我们就从 async() 说起。假如,我想在一个协程里,不断的发送网络请求的结果给另一个协程使用,按照当前我们所掌握的知识,可能会写出这种代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val deferred = scope.async {while (isActive) {gitHub.contributors("square", "retrofit")}}launch {delay(1000)println("Contributors: ${deferred.await()}")}delay(3000)
}
但这个代码是不可用的,因为 async() 只能一次性的发送一个数据,不能多次发送。
一个比较常见且易于理解的实际场景是,股票软件需要实时显示股票的价格,因此需要不停的去查询交易所的数据。这个不停查询的动作需要在一个协程中进行,然后查询的结果需要在另一个协程中显示在 UI 的文字和股价图表之中。
为了解决这个问题,可以对上述代码进行改造,使用 produce() 替代 async():
@OptIn(ExperimentalCoroutinesApi::class)
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)// 1.使用 produce 启动一个返回 ReceiveChannel 的协程val receiveChannel = scope.produce { // this:ProducerScopewhile (isActive) {val data = gitHub.contributors("square", "retrofit")// 2.使用 send 将数据发送给调用 receive 的协程send(data)}}launch {delay(1000)// 3.接收数据需调用 receivewhile (isActive) {// 持续接收数据println("Contributors: ${receiveChannel.receive()}")}}delay(3000)
}
这样就可以实现在一个协程中不断获取数据并发送给另一个协程的功能了。
需要注意的一些问题:
-
produce 与 launch 和 async 一样,都是协程启动器,只不过 produce 启动的协程会生产一个数据流给 Channel,再由 Channel 在需要使用数据流的协程中接收数据
-
produce 后有一个泛型,该泛型类型可以通过 send() 发送的数据推导出具体类型
-
produce 的 block 参数的接收者是 ProducerScope,是 CoroutineScope 的子类:
@ExperimentalCoroutinesApi public fun <E> CoroutineScope.produce(context: CoroutineContext = EmptyCoroutineContext,capacity: Int = 0,// launch 和 async 的 block 参数是 CoroutineScope@BuilderInference block: suspend ProducerScope<E>.() -> Unit ): ReceiveChannel<E> =produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block) -
ProducerScope 是 CoroutineScope 子接口,它还继承了 SendChannel 接口:
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {public val channel: SendChannel<E> }
3、Channel 的工作模式详解
Flow 的很多逻辑和 API 与 Channel 是相通的,所以对 Channel 的了解会对学习 Flow 有直接帮助。
前面讲 launch 时我们通过打 log 的方式证明了:
public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit
): Job {val newContext = newCoroutineContext(context)val coroutine = if (start.isLazy)LazyStandaloneCoroutine(newContext, block) elseStandaloneCoroutine(newContext, active = true)coroutine.start(start, coroutine, block)return coroutine
}
launch 返回的 Job 与 block 参数的 CoroutineScope 是同一个对象。对于 produce 也是类似的:
@ExperimentalCoroutinesApi
public fun <E> CoroutineScope.produce(context: CoroutineContext = EmptyCoroutineContext,capacity: Int = 0,@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> =produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block)
produce 返回的 ReceiveChannel 与 block 参数的 ProducerScope 是同一个对象。
再进一步看 produce() 的内容:
internal fun <E> CoroutineScope.produce(context: CoroutineContext = EmptyCoroutineContext,capacity: Int = 0,onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND,start: CoroutineStart = CoroutineStart.DEFAULT,onCompletion: CompletionHandler? = null,@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> {// 创建 Channelval channel = Channel<E>(capacity, onBufferOverflow)val newContext = newCoroutineContext(context)// 将 Channel 传入 ProducerCoroutine 协程中val coroutine = ProducerCoroutine(newContext, channel)if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)coroutine.start(start, coroutine, block)return coroutine
}
返回值 coroutine 的类型是 ProducerCoroutine,它继承了 ChannelCoroutine:
private class ProducerCoroutine<E>(parentContext: CoroutineContext, channel: Channel<E>
) : ChannelCoroutine<E>(parentContext, channel, true, active = true), ProducerScope<E> {...
}
ChannelCoroutine 通过参数传入的 _channel 以及类型声明上的 by _channel 形成接口委托,将 Channel 包在协程的内部:
internal open class ChannelCoroutine<E>(parentContext: CoroutineContext,protected val _channel: Channel<E>,initParentJob: Boolean,active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob, active), Channel<E> by _channel {val channel: Channel<E> get() = this...
}
Channel 是 SendChannel 和 ReceiveChannel 的子接口 :
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E>
实际上这意味着,produce() 封装了 Channel,由其提供底层支持。我们可以直接使用 Channel 的工厂函数创建 Channel 对象实现同样的操作:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)// 通过 Channel 工厂函数创建 Channel 对象,泛型是传输的数据类型val channel = Channel<List<Contributor>>()// 在一个协程发送数据scope.launch {channel.send(gitHub.contributors("square", "retrofit"))}// 另一个协程接收数据scope.launch {println("""Received data:${channel.receive()}""".trimIndent())}delay(3000)
}
Channel 的数据结构是一个挂起式的队列。它的功能定位类似于 BlockingQueue,只不过 BlockingQueue 在条件不满足时会阻塞线程,而 Channel 则是挂起协程。当 Channel 元素满了之后,再向 Channel 插入元素会挂起协程来等待空闲位置,取数据同理。
正是因为上述的本质,Channel 不适合做可订阅的事件流。示例代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val channel = Channel<List<Contributor>>()// 一个发送者,两个订阅者,发送者的数据只能被一个 send() 接收,意味着// 没有一个协程能完整的接收到所有数据scope.launch {channel.send(gitHub.contributors("square", "retrofit"))}scope.launch {channel.receive()}scope.launch {channel.receive()}delay(3000)
}
Channel 是一个队列,其他协程来队列取一次数据就把该数据取走了,其他协程再来取数据拿到的就是队列中的下一条数据。因此会出现如下情况,第一次 send 的数据可能会被第一个 receive 接收到,第二次 send 的数据就会被另一个协程的 receive 接收(就发一个数据,不可能被所有协程接收到)。因此当订阅者多于一个的时候,所有协程都接收不到完整数据。
其实在 Flow 的 API 诞生之后,Channel 已经慢慢退居二线了。现在它最主要的用处是作为 Flow API 的下层支持。比如 Flow 的 Buffer 功能就是用 Channel 实现的(用 Channel 把数据缓冲到另一个协程)。
4、Channel API 详解
Channel 的 API 很多与 Flow 都是相通的,我们先学习 Channel 的,后续对 Flow 的 API 理解会有很大帮助。
4.1 Channel 的遍历
之前我们举得例子,是通过 while(isActive) 从 Channel 中不断获取数据:
private fun getDataByWhile() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val channel = Channel<List<Contributor>>()scope.launch {channel.send(gitHub.contributors("square", "retrofit"))}launch {while (isActive) {val contributors = channel.receive()println("Contributors: $contributors")}}
}
实际上可以通过 for 循环来实现从 Channel 中取数据。for 循环的遍历都是通过对迭代器的实现而实现的,ReceiveChannel 在接口中定义了 iterator() 做运算符重载:
public interface ReceiveChannel<out E> {/*** 使用 for 循环返回一个新的迭代器,以从此通道接收元素。* 当通道[对于 receive 操作关闭][isClosedForReceive]且没有原因时,迭代会正常完成,* 如果通道处于失败状态,则会抛出原始的 [close][SendChannel.close] 原因异常。*/public operator fun iterator(): ChannelIterator<E>
}
示例代码:
private fun getDataByFor() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val channel = Channel<List<Contributor>>()scope.launch {channel.send(gitHub.contributors("square", "retrofit"))}launch {// 挂起式遍历for (contributors in channel) {println("Contributors: $contributors")}}
}
Channel 的遍历相比于一般的容器比较特殊,它的遍历是挂起式的。它在没有元素时,会把协程挂起,直到下一个元素出现。当 Channel 关闭后,循环遍历也就结束了。
类似地,当 Channel 队列满了之后,如果再调用 send 这个挂起函数试图向 Channel 中增加元素,也会导致协程被挂起,直到 Channel 中有空闲位置之后。但是,需要注意,Channel 队列默认长度为 0,也就是说第一次调用 send 会导致协程挂起,除非在此之前,已经有其他协程调用了 receive。但是先调用 receive 的协程会因为 Channel 中没有元素被挂起。
4.2 Channel 工厂函数
可以通过 Channel 的工厂函数参数指定 Channel 的容量:
/**
* 使用指定的缓冲区容量(或默认情况下不使用缓冲区)创建一个通道。详细信息,请参阅 [Channel] 接口文档。
*
* @param capacity 正数通道容量或在 [Channel.Factory] 中定义的常量之一。
* @param onBufferOverflow 配置缓冲区溢出时的操作(可选,默认为尝试通过 [send][Channel.send]
* 发送值的 [挂起][BufferOverflow.SUSPEND],仅在 capacity >= 0 或 capacity == Channel.BUFFERED
* 时支持,隐式地创建至少一个缓冲元素的通道)。
* @param onUndeliveredElement 可选函数,当元素已发送但未传递给消费者时调用。
* @throws IllegalArgumentException 当 [capacity] < -2 时抛出。
*/
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)}}
capacity 默认值是 RENDEZVOUS = 0,此时数据的发送与接收必须通过“见面”的方式完成。
“Rendezvous” 是法语单词,意为“约会”或“会面”。在计算机科学领域,特别是在并发编程中,“rendezvous” 通常用于描述两个或多个进程或线程在某个特定点上同时等待彼此的情况。
在并发编程中,“rendezvous” 可以指以下情况之一:
- 进程或线程之间的同步:当两个或多个进程或线程需要在某个特定点上同时到达以继续执行,这种同步等待的过程可以称为"rendezvous"。这有助于确保进程或线程在合适的时机相遇,并且在适当的时候进行交互或数据传递。
- 消息传递:在消息传递模型中,“rendezvous” 可以表示发送者和接收者之间的一种同步机制。发送者在发送消息时等待接收者准备好接收消息,而接收者在接收消息时等待发送者准备好发送消息,以确保消息能够正确传递。
从上述源码不难看出,Channel 提供了几种预置的数值供 capacity 选择,不同的类型创建不同的 Channel。这几个预置值如下:
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E> {public companion object Factory {// Channel 的 buffer 没有容量限制public const val UNLIMITED: Int = Int.MAX_VALUE// Channel 没有 bufferpublic const val RENDEZVOUS: Int = 0// 用于创建合并通道,相当于使用 [onBufferOverflow = DROP_OLDEST][BufferOverflow.DROP_OLDEST]public const val CONFLATED: Int = -1// 用于请求具有默认缓冲区容量的缓冲通道。// 对于在溢出时[挂起][BufferOverflow.SUSPEND]的通道,默认容量为 64,并可通过在 JVM 上设置// [DEFAULT_BUFFER_PROPERTY_NAME] 进行覆盖。// 对于非挂起通道,使用容量为 1 的缓冲区。public const val BUFFERED: Int = -2}
}
观察 BUFFERED 参数的注释,它会在不同的溢出处理策略设置不同的容量。这个策略就是 Channel 工厂函数的第二个参数 onBufferOverflow,默认值是 BufferOverflow.SUSPEND,共有三个值可选:
public enum class BufferOverflow {/*** 在缓冲区溢出时挂起*/SUSPEND,/*** 在溢出时删除缓冲区中最旧的值,将新值添加到缓冲区,不会挂起。*/DROP_OLDEST,/*** 在缓冲区溢出时删除当前正在添加到缓冲区中的最新值(以使缓冲区内容保持不变),不会挂起。*/DROP_LATEST
}
比较常用的是 SUSPEND 与 DROP_OLDEST(丢弃队首的旧元素),比如我们前面在说 Channel 的第一个参数 capacity 时介绍过有一个可选值 CONFLATED,它的作用就相当于创建了容量为 1,溢出策略为 BufferOverflow.DROP_OLDEST 的 Channel:
fun main() = runBlocking<Unit> {// 二者等价,但是如果填了 CONFLATED,那么就要求溢出策略必须是默认的 SUSPENDval channel1 = Channel<List<Contributor>>(1, BufferOverflow.DROP_OLDEST)val channel2 = Channel<List<Contributor>>(CONFLATED/*, BufferOverflow.SUSPEND*/)
}
丢弃数据也是有用处的,比如对于不断提供界面数据的 Channel,界面会用你提供的最新的数据来更新界面。如果发送数据的频率高于界面处理数据(比如经过某种耗时计算之后才能显示到界面)的速度,可能会出现下游没有把上一条数据处理完,上游就发送来多条新的数据堆积在队列中。为了显示最新的数据,可以丢弃掉队列中堆积的旧数据,直接取最新的一条。
4.3 Channel 的关闭
Channel 的关闭有两种方式:close() 与 cancel()。
了解 API 之前先了解,什么是关闭,为什么要关闭。
Channel 可以是一个事件流,也可以是数据流(具体是哪种流看你怎么用它)。流在某一个时间后可能不再发送或接收数据了,那就需要关闭它。比如一个传递网络数据给界面的 Channel,在网络数据的持续获取结束之后,或者在界面组件从界面里移除之后,就可以将 Channel 关闭。
Channel 的两个关闭函数属于两个不同接口,Channel 实现了 SendChannel 和 ReceiveChannel,close() 属于 SendChannel:
public interface SendChannel<in E> {@DelicateCoroutinesApipublic val isClosedForSend: Booleanpublic suspend fun send(element: E)public fun close(cause: Throwable? = null): Boolean
}
isClosedForSend 表示是否已经关闭发送功能,调用了 close() 之后,isClosedForSend 被修改为 true,这之后就不允许再调用 send() 了,否则就会抛出 ClosedSendChannelException:
public class ClosedSendChannelException(message: String?) : IllegalStateException(message)
当然,由于已经 send() 出去的数据不可能立即、马上就被接收端收到,因此缓冲区中的数据可能会存留一段时间。在这段时间内,还可以调用 receive() 来接收数据。但是当所有数据都被接收完毕后,Channel 会将 ReceiveChannel 这个接口的 isClosedForReceive 修改为 true,意思是这之后不能再调用 receive(),否则会抛 ClosedReceiveChannelException:
public class ClosedReceiveChannelException(message: String?) : NoSuchElementException(message)
以上是 Channel 在发送端的关闭过程。下面再看 Channel 在接收端关闭。
假如接收端不再需要接收数据了,比如更新界面的协程,它需要更新的界面组件被移除了,那就不再需要新元素了。这种情况下可以直接调用 ReceiveChannel 的 cancel(),它会把 SendChannel 的 isClosedForSend 和 ReceiveChannel 的 isClosedForReceive 都修改为 true,禁止调用 send() 和 receive()。如果在 cancel() 之后还调用 send() 或 receive(),会抛出 CancellationException,用以区分是发送端还是接收端关闭导致的异常。以便针对不同的异常去写业务代码。
close() 也可以传入自定义的异常,在触发异常后就会抛这个指定的异常。比如:
channel.close(IllegalStateException("Data error!"))
Channel 在调用 cancel() 之后,那些已经发送但是还没被接收的数据就没用了。但是这些数据如果直接丢弃,可能会造成某种资源泄漏。比如,发送的是文件流:
val fileChannel = Channel<FileWriter>()
fileChannel.send(FileWriter("test.txt"))
本来,应该是接收到 FileWriter 使用完毕后将其关闭的。但是,在 Channel 调用 cancel() 之后,尚未被接收的 FileWriter 就被丢到外太空去了,进而造成资源泄漏。此时需要使用 Channel 的第三个参数 —— onUndeliveredElement() 来处理未交接的元素。
比如对于 FileWriter 而言,可以这样处理:
// onUndeliveredElement: ((E) -> Unit)? = null
val fileChannel = Channel<FileWriter> { it.close() }
这样,在 FileWriter 被丢弃之前,会先执行它的 close() 避免资源泄漏。
4.4 trySend() 与 tryReceive()
这一对函数是 send() 与 receive() 的兄弟函数,它们不是挂起函数,会瞬时返回。如果因为缓冲满了无法发送数据,或者因为缓冲中没有数据而无法接收数据,它们都不会等待,而是直接返回,只不过返回的是失败的结果。
二者的返回值类型是 ChannelResult:
@JvmInline
public value class ChannelResult<out T>
@PublishedApi internal constructor(@PublishedApi internal val holder: Any?) {public val isSuccess: Boolean get() = holder !is Failedpublic val isFailure: Boolean get() = holder is Failedpublic val isClosed: Boolean get() = holder is Closed...
}
如果成功发送或接收数据,那么 isSuccess 就为 true,isFailure 为 false;如果失败的话就反过来。isClosed 是特别的失败类型,如果是因为 Channel 关闭而失败,则 isClosed 为 true;如果是因为缓冲满了暂时没法写,或者是缓冲空了暂时没法读,这种失败 isClosed 为 false。
4.5 其他函数
// 如果此实例表示成功,则返回封装的值;如果表示失败,则返回 null@Suppress("UNCHECKED_CAST")public fun getOrNull(): T? = if (holder !is Failed) holder as T else null// 如果此实例表示成功,则返回封装的值;如果已关闭或失败,则抛出异常public fun getOrThrow(): T {@Suppress("UNCHECKED_CAST")if (holder !is Failed) return holder as Tif (holder is Closed && holder.cause != null) throw holder.causeerror("Trying to call 'getOrThrow' on a failed channel result: $holder")}// 如果此实例表示失败,则返回封装的异常;如果表示成功或对已关闭通道的操作不成功,则返回 nullpublic fun exceptionOrNull(): Throwable? = (holder as? Closed)?.cause// 发生异常时不抛异常,而是将异常封进 ChannelResult 中public suspend fun receiveCatching(): ChannelResult<E>
5、actor():把 SendChannel 暴露出来
前面讲过,可以将 Channel 的创建、发送与接收工作分开写:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val channel = Channel<Int>()scope.launch {for (num in 1..100) {channel.send(num)delay(100)}}scope.launch {for (num in channel) {println("Number: $num")}}delay(10000)
}
也可以通过 produce() 将创建和发送操作合并以简化代码:
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val channel = scope.produce {for (num in 1..100) {send(num)delay(100)}}scope.launch {for (num in channel) {println("Number: $num")}}delay(10000)
}
与 produce() 相反的,还有一个启动器 actor():
- produce() 是启动一个协程,将 Channel 创建与发送数据的操作合并在一起,返回 ReceiveChannel
- actor() 也是启动一个协程,将 Channel 创建与接收数据的操作合并在一起,返回 SendChannel
使用 actor() 改造上述代码:
@OptIn(ObsoleteCoroutinesApi::class)
fun main() = runBlocking<Unit> {val scope = CoroutineScope(EmptyCoroutineContext)val sendChannel = scope.actor<Int> {for (num in this) {println("Number: $num")}}scope.launch {for (num in 1..100) {sendChannel.send(num)delay(100)}}delay(10000)
}
需要注意 actor() 是被打了 @ObsoleteCoroutinesApi 注解的:
/**
* 在协程 API 中标记已过时的声明,意味着相应声明的设计存在严重已知缺陷,将来会重新设计。
* 大致来说,这些声明将来会被弃用,但目前还没有替代方案,因此不能立即弃用它们。
*/
@MustBeDocumented
@Retention(value = AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
public annotation class ObsoleteCoroutinesApi
也就是说,@ObsoleteCoroutinesApi 要比 @Deprecated 的严重性要弱一点。这意味着 actor() 可以用,但是未来可能随着协程 API 的重新设计,需要跟随它的变化对 actor() 的用法做出相应的修改。但是现在要使用的话,必须带上 @OptIn(ObsoleteCoroutinesApi::class)。
相关文章:
Coroutine 基础五 —— Flow 之 Channel 篇
1、Channel 与 Flow 简介与对比 所有知识都可总结为一个字 —— 流。包括数据流、事件流、状态流。 开发中最常用的 StateFlow 提供状态订阅。可以将一些信息包进 StateFlow 中进行保存。比如界面上显示的字符串,或者系统级别的信息,如用户状态。装进 …...
快速掌握Elasticsearch检索之二:滚动查询(scrool)获取全量数据(golang)
Elasticsearch8.17.0在mac上的安装 Kibana8.17.0在mac上的安装 Elasticsearch检索方案之一:使用fromsize实现分页 1、滚动查询的使用场景 滚动查询区别于上一篇文章介绍的使用from、size分页检索,最大的特点是,它能够检索超过10000条外的…...
C++设计模式:状态模式(自动售货机)
什么是状态模式? 状态模式是一种行为型设计模式,它允许一个对象在其内部状态发生改变时,动态改变其行为。通过将状态相关的逻辑封装到独立的类中,状态模式能够将状态管理与行为解耦,从而让系统更加灵活和可维护。 通…...
【网络安全实验室】脚本关实战详情
难道向上攀爬的那条路,不是比站在顶峰更让人热血澎湃吗 1.key又又找不到了 点击链接,burp抓包,发送到重放模块,点击go 得到key 2.快速口算 python3脚本 得到key 3.这个题目是空的 试了一圈最后发现是 4.怎么就是不弹出key呢…...
ts总结一下
ts基础应用 /*** 泛型工具类型*/ interface IProps {id: string;title: string;children: number[]; } type omita Omit<IProps, id | title>; const omitaA: omita {children: [1] }; type picka Pick<IProps, id | title>; const pickaA: picka {id: ,title…...
MySQL数据库笔记——主从复制
大家好,这里是Good Note,关注 公主号:Goodnote,本文详细介绍 MySQL的主从复制,从原理到配置再到同步过程。 文章目录 简介核心组件主从复制的原理作用主从复制的线程模型主从复制的模式形式复制的方式设计复制机制主从…...
OpenAI发布o3:圣诞前夜的AI惊喜,颠覆性突破还是技术焦虑?
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...
欧拉-伯努利梁自由波动的频散关系
梁和杆都是一维结构,但是梁的弯曲波比杆的纵波要复杂多。例如即使最简单的欧拉-伯努利(Euler-Bernoulli)梁的弯曲波也具有频散特征,且当梁的特征尺寸和弯曲波波长满足某个比值时,欧拉-伯努利梁不再适用,需要引入铁摩辛克(Timoshenko)梁模型。 考察某一欧拉-伯努利梁,长度…...
Cursor小试1.生成一个网页的接口请求工具
一般开发过程中,会涉及到接口的调试,往往有时候开发的电脑不是我们自己的,没有安装一些类似postman 的接口调用工具,所以发现问题或者要测试某些接口是否正常的时候会很麻烦,而且现在网上也没有找到很好的免费的网页端接口请求的网址,所以我们使用Cursor来编写这样一个小工具, …...
Xilinx DCI技术
Xilinx DCI技术 DCI技术概述Xilinx DCI技术实际使用某些Bank特殊DCI要求 DCI级联技术DCI端接方式阻抗控制驱动器(源端接)半阻抗控制阻抗驱动器(源端接)分体式DCI(戴维宁等效端接到VCCO/2)DCI和三态DCI&…...
Kubernetes Pod 优雅关闭:如何让容器平稳“退休”?
Kubernetes Pod 优雅关闭:如何让容器平稳“退休”? 在 Kubernetes 中,Pod 是应用的基本单元。你可能会遇到需要停止某个 Pod 或容器的情况,可能是因为要更新、调整或故障恢复。在这种情况下,Pod 的优雅关闭࿰…...
鸿蒙应用开发(1)
可能以为通过 鸿蒙应用开发启航计划(点我去看上一节) 的内容,就足够了,其实还没有。 可是我还是要告诉你,你还需要学习新的语言 -- ArkTS。 ,ArkTS是HUAWEI开发的程序语言。你需要学习这门语言。这会花费你…...
SimForge HSF 案例分享|复杂仿真应用定制——UAVSim无人机仿真APP(技术篇)
导读 「神工坊」核心技术——「SimForge HSF高性能数值模拟引擎」支持工程计算应用的快速开发、自动并行,以及多域耦合、AI求解加速,目前已实现航发整机数值模拟等多个系统级高保真数值模拟应用落地,支持10亿阶、100w核心量级的高效求解。其低…...
使用 Adaptive Mesh Refinement 加速 CFD 仿真:最佳实践
CFD 仿真中的网格划分挑战 技术的进步正在增强设计探索,数值仿真在优化工程设计方面发挥着至关重要的作用。通常,计算流体动力学 (CFD) 仿真从定制的手工网格开始,具有精细和粗糙的区域,以平衡分辨率和单元…...
前端-动画库Lottie 3分钟学会使用
目录 1. Lottie地址 2. 使用html实操 3. 也可以选择其他的语言 1. Lottie地址 LottieFiles: Download Free lightweight animations for website & apps.Effortlessly bring the smallest, free, ready-to-use motion graphics for the web, app, social, and designs.…...
智能工厂的设计软件 应用场景的一个例子:为AI聊天工具添加一个知识系统 之5
本文要点 前端 问题描述语言 本文继续完善 “描述” ---现在我们应该可以将它称为 “问题problem描述语言 ”。 它 通过对话框的question 引发 表征的issue 的“涌现” 最终 厘清应用程序的“problem”。即它合并了 ISO七层模型中的上面三层,通过将三层 分别形成…...
java web
流程 1.浏览器发送http协议的格式数据和url给服务器软件tomcat 2.浏览器解析http格式数据并创建request和response对象,把数据封装到request对象里。 3.tomcat解析url确定访问路径,如果是静态资源html等,直接将html数据作为http格式响应体返回&#x…...
【嵌入式软件开发】嵌入式软件计时逻辑的两种实现:累加与递减的深入对比
本文主要从四个方面详细阐述了嵌入式软件编程中计时逻辑的两种实现方式:累加和递减。让我为您详细解析各个部分: 1. 基本概念对比 累加方式 从0开始向上计数每个周期增加固定值(通常为1)类似于我们日常生活中的秒表计时方式递减方式 从预设值开始向下计数每个周期减少固定…...
如何将vCenter6.7升级7.0?
vCenter是什么? vCenter是一种虚拟化管理软件,由VMware公司开发和发布。它是VMware vSphere虚拟化平台的核心组件之一,主要用于集中管理和监控虚拟化环境中的虚拟机、虚拟存储和网络资源。vCenter可以实现对多个ESXi主机的集中管理ÿ…...
服务器网卡绑定mode和交换机的对应关系
互联网各领域资料分享专区(不定期更新): Sheet 模式类别 网卡绑定mode共有七种(0~6): bond0、bond1、bond2、bond3、bond4、bond5、bond6 mode详解 mode0 ,即:(balance-rr) Round-robin policy(平衡轮循环策略,需要配置交换机静态聚合) mode…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
回溯算法学习
一、电话号码的字母组合 import java.util.ArrayList; import java.util.List;import javax.management.loading.PrivateClassLoader;public class letterCombinations {private static final String[] KEYPAD {"", //0"", //1"abc", //2"…...
