当前位置: 首页 > news >正文

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” 可以指以下情况之一:

  1. 进程或线程之间的同步:当两个或多个进程或线程需要在某个特定点上同时到达以继续执行,这种同步等待的过程可以称为"rendezvous"。这有助于确保进程或线程在合适的时机相遇,并且在适当的时候进行交互或数据传递。
  2. 消息传递:在消息传递模型中,“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 中进行保存。比如界面上显示的字符串&#xff0c;或者系统级别的信息&#xff0c;如用户状态。装进 …...

快速掌握Elasticsearch检索之二:滚动查询(scrool)获取全量数据(golang)

Elasticsearch8.17.0在mac上的安装 Kibana8.17.0在mac上的安装 Elasticsearch检索方案之一&#xff1a;使用fromsize实现分页 1、滚动查询的使用场景 滚动查询区别于上一篇文章介绍的使用from、size分页检索&#xff0c;最大的特点是&#xff0c;它能够检索超过10000条外的…...

C++设计模式:状态模式(自动售货机)

什么是状态模式&#xff1f; 状态模式是一种行为型设计模式&#xff0c;它允许一个对象在其内部状态发生改变时&#xff0c;动态改变其行为。通过将状态相关的逻辑封装到独立的类中&#xff0c;状态模式能够将状态管理与行为解耦&#xff0c;从而让系统更加灵活和可维护。 通…...

【网络安全实验室】脚本关实战详情

难道向上攀爬的那条路&#xff0c;不是比站在顶峰更让人热血澎湃吗 1.key又又找不到了 点击链接&#xff0c;burp抓包&#xff0c;发送到重放模块&#xff0c;点击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数据库笔记——主从复制

大家好&#xff0c;这里是Good Note&#xff0c;关注 公主号&#xff1a;Goodnote&#xff0c;本文详细介绍 MySQL的主从复制&#xff0c;从原理到配置再到同步过程。 文章目录 简介核心组件主从复制的原理作用主从复制的线程模型主从复制的模式形式复制的方式设计复制机制主从…...

OpenAI发布o3:圣诞前夜的AI惊喜,颠覆性突破还是技术焦虑?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

欧拉-伯努利梁自由波动的频散关系

梁和杆都是一维结构,但是梁的弯曲波比杆的纵波要复杂多。例如即使最简单的欧拉-伯努利(Euler-Bernoulli)梁的弯曲波也具有频散特征,且当梁的特征尺寸和弯曲波波长满足某个比值时,欧拉-伯努利梁不再适用,需要引入铁摩辛克(Timoshenko)梁模型。 考察某一欧拉-伯努利梁,长度…...

Cursor小试1.生成一个网页的接口请求工具

一般开发过程中,会涉及到接口的调试,往往有时候开发的电脑不是我们自己的,没有安装一些类似postman 的接口调用工具,所以发现问题或者要测试某些接口是否正常的时候会很麻烦,而且现在网上也没有找到很好的免费的网页端接口请求的网址,所以我们使用Cursor来编写这样一个小工具, …...

Xilinx DCI技术

Xilinx DCI技术 DCI技术概述Xilinx DCI技术实际使用某些Bank特殊DCI要求 DCI级联技术DCI端接方式阻抗控制驱动器&#xff08;源端接&#xff09;半阻抗控制阻抗驱动器&#xff08;源端接&#xff09;分体式DCI&#xff08;戴维宁等效端接到VCCO/2&#xff09;DCI和三态DCI&…...

Kubernetes Pod 优雅关闭:如何让容器平稳“退休”?

Kubernetes Pod 优雅关闭&#xff1a;如何让容器平稳“退休”&#xff1f; 在 Kubernetes 中&#xff0c;Pod 是应用的基本单元。你可能会遇到需要停止某个 Pod 或容器的情况&#xff0c;可能是因为要更新、调整或故障恢复。在这种情况下&#xff0c;Pod 的优雅关闭&#xff0…...

鸿蒙应用开发(1)

可能以为通过 鸿蒙应用开发启航计划&#xff08;点我去看上一节&#xff09; 的内容&#xff0c;就足够了&#xff0c;其实还没有。 可是我还是要告诉你&#xff0c;你还需要学习新的语言 -- ArkTS。 &#xff0c;ArkTS是HUAWEI开发的程序语言。你需要学习这门语言。这会花费你…...

SimForge HSF 案例分享|复杂仿真应用定制——UAVSim无人机仿真APP(技术篇)

导读 「神工坊」核心技术——「SimForge HSF高性能数值模拟引擎」支持工程计算应用的快速开发、自动并行&#xff0c;以及多域耦合、AI求解加速&#xff0c;目前已实现航发整机数值模拟等多个系统级高保真数值模拟应用落地&#xff0c;支持10亿阶、100w核心量级的高效求解。其低…...

使用 Adaptive Mesh Refinement 加速 CFD 仿真:最佳实践

CFD 仿真中的网格划分挑战 技术的进步正在增强设计探索&#xff0c;数值仿真在优化工程设计方面发挥着至关重要的作用。通常&#xff0c;计算流体动力学 &#xff08;CFD&#xff09; 仿真从定制的手工网格开始&#xff0c;具有精细和粗糙的区域&#xff0c;以平衡分辨率和单元…...

前端-动画库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七层模型中的上面三层&#xff0c;通过将三层 分别形成…...

java web

流程 1.浏览器发送http协议的格式数据和url给服务器软件tomcat 2.浏览器解析http格式数据并创建request和response对象,把数据封装到request对象里。 3.tomcat解析url确定访问路径&#xff0c;如果是静态资源html等&#xff0c;直接将html数据作为http格式响应体返回&#x…...

【嵌入式软件开发】嵌入式软件计时逻辑的两种实现:累加与递减的深入对比

本文主要从四个方面详细阐述了嵌入式软件编程中计时逻辑的两种实现方式:累加和递减。让我为您详细解析各个部分: 1. 基本概念对比 累加方式 从0开始向上计数每个周期增加固定值(通常为1)类似于我们日常生活中的秒表计时方式递减方式 从预设值开始向下计数每个周期减少固定…...

如何将vCenter6.7升级7.0?

vCenter是什么&#xff1f; vCenter是一种虚拟化管理软件&#xff0c;由VMware公司开发和发布。它是VMware vSphere虚拟化平台的核心组件之一&#xff0c;主要用于集中管理和监控虚拟化环境中的虚拟机、虚拟存储和网络资源。vCenter可以实现对多个ESXi主机的集中管理&#xff…...

服务器网卡绑定mode和交换机的对应关系

互联网各领域资料分享专区(不定期更新)&#xff1a; Sheet 模式类别 网卡绑定mode共有七种(0~6): bond0、bond1、bond2、bond3、bond4、bond5、bond6 mode详解 mode0 &#xff0c;即:(balance-rr) Round-robin policy(平衡轮循环策略&#xff0c;需要配置交换机静态聚合) mode…...

Maven (day04)

什么是maven? Maven 是 Apache 旗下的一个开源项目&#xff0c;是一款用于管理和构建 java 项目的工具。 官网&#xff1a;Welcome to Apache Maven – Maven https://maven.apache.org/ Maven的作用 依赖管理&#xff08;方便快捷的管理项目依赖的资源(jar包)&#xff…...

Echart实现3D饼图示例

在可视化项目中&#xff0c;很多地方会遇见图表&#xff1b;echart是最常见的&#xff1b;这个示例就是用Echart&#xff0c; echart-gl实现3D饼图效果&#xff0c;复制即可用 //需要安装&#xff0c;再引用依赖import * as echarts from "echarts"; import echar…...

UE5 Debug的一些心得

1、BUG粗略可分为两类&#xff1a; 一种是显性的&#xff0c;编译直接就通不过&#xff0c;必须马上解决。 第二种是隐性的&#xff0c;新功能完成后&#xff0c;编译成功顺利运行&#xff0c;洋洋自得&#xff0c;而问题隐藏在幕后&#xff0c;测试之后才逐渐发现有问题&…...

java中多线程的一些常见操作

Java 中的多线程是通过并发编程来提高应用程序的效率和响应速度。Java 提供了多个机制和类来支持多线程编程&#xff0c;包括继承 Thread 类、实现 Runnable 接口、使用线程池等。以下是 Java 中一些常见的多线程操作和应用场景。 1. 创建线程 1.1 通过继承 Thread 类创建线程…...

【gopher的java学习笔记】什么是Spring - IoC和DI

一聊到java&#xff0c;离不开的一个东西就是spring&#xff1b;当我想了解什么是spring的时候&#xff0c;一查&#xff0c;基本上都是围绕着两个词来展开的&#xff1a;IoC和AOP。 对于我自己来说&#xff0c;AOP我觉得比较好理解&#xff0c;因为不管是之前写golang还是pyt…...

【开源免费】基于SpringBoot+Vue.JS校园社团信息管理系统(JAVA毕业设计)

本文项目编号 T 107 &#xff0c;文末自助获取源码 \color{red}{T107&#xff0c;文末自助获取源码} T107&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…...

设计模式 创建型 工厂模式(Factory Pattern)与 常见技术框架应用 解析

工厂模式&#xff08;Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一种封装对象创建过程的方式&#xff0c;使得对象的创建与使用分离&#xff0c;从而提高了系统的可扩展性和可维护性。 一、核心思想 工厂模式的核心思想是将“实例化对象”的操作与…...

pip 下载安装时使用国内源配置

pip 是 Python 的包管理工具&#xff0c;用于安装和管理第三方库。然而&#xff0c;在某些情况下&#xff0c;默认的 PyPI&#xff08;Python Package Index&#xff09;源可能由于网络原因导致下载速度慢或者连接不稳定。幸运的是&#xff0c;我们可以轻松地配置 pip 使用国内…...

【数据结构】数据结构简要介绍

数据结构是计算机科学中用于组织、管理和存储数据的方式&#xff0c;以便于高效地访问和修改数据。 数据结构的分类&#xff1a; 数据结构可以大致分为两类&#xff1a;线性结构和非线性结构。 1. 线性结构 线性结构中的数据按顺序排列&#xff0c;每个元素有唯一的前驱和后…...

数据分析-Excel

数据类型和函数初步 Excel中有文本类型和数值类型–但是无法用肉眼分辨出来isnumber来区分是否是数值类型text和value函数可以完成数值类型以及文本类型的转换单元格第一位输入’方式明确输入的是文本sum函数必须是数值类型 文本连接-and-or-not-if-mod-max函数 字符串的连接…...