阿里P7大佬教你破解 Kotlin 协程(3) - 协程调度篇

[译] 2019 年的 Android 网络 —— Retrofit 与 Kotlin 协程 //Kotlin Coroutines implementation “org.jetbrains.kotlinx:kotlinx-coroutines-android:kotlinCoroutineVersion"implementation"org.jetbrains.kotlinx:kotlinx−coroutines−core:kotlinCoroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:k 阅读详情

破解 Kotlin 协程(3) - 协程调度篇

1. 协程上下文

调度器本质上就是一个协程上下文的实现,我们先来介绍下上下文。

前面我们提到 launch 函数有三个参数,第一个参数叫 上下文,它的接口类型是 CoroutineContext,通常我们见到的上下文的类型是 CombinedContext 或者 EmptyCoroutineContext,一个表示上下文的组合,另一个表示什么都没有。我们来看下 CoroutineContext 的接口方法:

@SinceKotlin("1.3")
public interface CoroutineContext {
    public operator fun <E : Element> get(key: Key<E>): E?
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    public operator fun plus(context: CoroutineContext): CoroutineContext = ...
    public fun minusKey(key: Key<*>): CoroutineContext

    public interface Key<E : Element>

    public interface Element : CoroutineContext {
        public val key: Key<*>
        ...
    }
}

不知道大家有没有发现,它简直就是一个以 Key 为索引的 List:

|
CoroutineContext

|

List

|
| — | — |
|

get(Key)

|

get(Int)

|
|

plus(CoroutineContext)

|

plus(List)

|
|

minusKey(Key)

|

removeAt(Int)

|

表中的 List.plus(List) 实际上指的是扩展方法 Collection<T>.plus(elements:Iterable<T>):List<T>

CoroutineContext 作为一个集合,它的元素就是源码中看到的 Element,每一个 Element 都有一个 key,因此它可以作为元素出现,同时它也是 CoroutineContext 的子接口,因此也可以作为集合出现。

讲到这里,大家就会明白, CoroutineContext 原来是个数据结构啊。如果大家对于 List 的递归定义比较熟悉的话,那么对于 CombinedContext 和 EmptyCoroutineContext 也就很容易理解了,例如 scala 的 List是这么定义的:

sealed abstract class List[+A] extends ... {
    ...
    def head: A
    def tail: List[A]
    ...
}

在模式匹配的时候, List(1,2,3,4) 是可以匹配 x::y 的, x 就是 1, y 则是 List(2,3,4)。

CombinedContext 的定义也非常类似:

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    ...
}

只不过它是反过来的,前面是集合,后面是单独的一个元素。我们在协程体里面访问到的 coroutineContext大多是这个 CombinedContext 类型,表示有很多具体的上下文实现的集合,我们如果想要找到某一个特别的上下文实现,就需要用对应的 Key 来查找,例如:

suspend fun main(){
    GlobalScope.launch {
        println(coroutineContext[Job]) // "coroutine#1":StandaloneCoroutine{Active}@1ff62014
    }
    println(coroutineContext[Job]) // null,suspend main 虽然也是协程体,但它是更底层的逻辑,因此没有 Job 实例
}

这里的 Job 实际上是对它的 companionobject 的引用

public interface Job : CoroutineContext.Element {
    /**
     * Key for [Job] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<Job> { ... }
    ...
}

所以我们也可以仿照 Thread.currentThread() 来一个获取当前 Job 的方法:

suspend inline fun Job.Key.currentJob() = coroutineContext[Job]

suspend fun coroutineJob(){
    GlobalScope.launch {
        log(Job.currentJob())
    }
    log(Job.currentJob())
}

我们可以通过指定上下文为协程添加一些特性,一个很好的例子就是为协程添加名称,方便调试:

GlobalScope.launch(CoroutineName("Hello")) {
    ...
}

复制

如果有多个上下文需要添加,直接用 + 就可以了:

GlobalScope.launch(Dispatchers.Main + CoroutineName("Hello")) {
    ...
}

Dispatchers.Main 是调度器的一个实现,不用担心,我们很快就会认识它了。

2. 协程拦截器

费了好大劲儿说完上下文,这里就要说一个比较特殊的存在了——拦截器。

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    ...
}

拦截器也是一个上下文的实现方向,拦截器可以左右你的协程的执行,同时为了保证它的功能的正确性,协程上下文集合永远将它放在最后面,这真可谓是天选之子了。

它拦截协程的方法也很简单,因为协程的本质就是回调 + “黑魔法”,而这个回调就是被拦截的 Continuation 了。用过 OkHttp 的小伙伴一下就兴奋了,拦截器我常用的啊,OkHttp 用拦截器做缓存,打日志,还可以模拟请求,协程拦截器也是一样的道理。调度器就是基于拦截器实现的,换句话说调度器就是拦截器的一种。

我们可以自己定义一个拦截器放到我们的协程上下文中,看看会发生什么。

class MyContinuationInterceptor: ContinuationInterceptor{
    override val key = ContinuationInterceptor
    override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}

class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
    override val context = continuation.context
    override fun resumeWith(result: Result<T>) {
        log("<MyContinuation> $result" )
        continuation.resumeWith(result)
    }
}

我们只是在回调处打了一行日志。接下来我们把用例拿出来:

suspend fun main() {
    GlobalScope.launch(MyContinuationInterceptor()) {
        log(1)
        val job = async {
            log(2)
            delay(1000)
            log(3)
            "Hello"
        }
        log(4)
        val result = job.await()
        log("5. $result")
    }.join()
    log(6)
}

这可能是迄今而止我们给出的最复杂的例子了,不过请大家不要被它吓到,它依然很简单。我们通过 launch启动了一个协程,为它指定了我们自己的拦截器作为上下文,紧接着在其中用 async 启动了一个协程, async 与 launch 从功能上是同等类型的函数,它们都被称作协程的 Builder 函数,不同之处在于 async 启动的 Job 也就是实际上的 Deferred 可以有返回结果,可以通过 await 方法获取。

可想而知, result 的值就是 Hello。那么这段程序运行的结果如何呢?

15:31:55:989 [main] <MyContinuation> Success(kotlin.Unit)  // ①
15:31:55:992 [main] 1
15:31:56:000 [main] <MyContinuation> Success(kotlin.Unit) // ②
15:31:56:000 [main] 2
15:31:56:031 [main] 4
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(kotlin.Unit) // ③
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] 3
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(Hello) // ④
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 5. Hello
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 6

“// ①” 不是程序输出的内容,仅为后续讲解方便而做的标注。

大家可能就要奇怪了,你不是说 Continuation 是回调么,这里面回调调用也就一次啊( await 那里),怎么日志打印了四次呢?

别慌,我们按顺序给大家介绍。

首先,所有协程启动的时候,都会有一次 Continuation.resumeWith 的操作,这一次操作对于调度器来说就是一次调度的机会,我们的协程有机会调度到其他线程的关键之处就在于此。 ①、② 两处都是这种情况。

其次, delay 是挂起点,1000ms 之后需要继续调度执行该协程,因此就有了 ③ 处的日志。

最后,④ 处的日志就很容易理解了,正是我们的返回结果。

可能有朋友还会有疑问,我并没有在拦截器当中切换线程,为什么从 ③ 处开始有了线程切换的操作?这个切换线程的逻辑源自于 delay,在 JVM 上 delay 实际上是在一个 ScheduledExcecutor 里面添加了一个延时任务,因此会发生线程切换;而在 JavaScript 环境中则是基于 setTimeout,如果运行在 Nodejs 上, delay就不会切线程了,毕竟人家是单线程的。

如果我们在拦截器当中自己处理了线程切换,那么就实现了自己的一个简单的调度器,大家有兴趣可以自己去尝试。

思考:拦截器可以有多个吗?

3. 调度器

3.1 概述

有了前面的基础,我们对于调度器的介绍就变得水到渠成了。

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    ...
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    ...
}

它本身是协程上下文的子类,同时实现了拦截器的接口, dispatch 方法会在拦截器的方法 interceptContinuation 中调用,进而实现协程的调度。所以如果我们想要实现自己的调度器,继承这个类就可以了,不过通常我们都用现成的,它们定义在 Dispatchers 当中:

val Default: CoroutineDispatcher
val Main: MainCoroutineDispatcher
val Unconfined: CoroutineDispatcher

这个类的定义涉及到了 Kotlin MPP 的支持,因此你在 Jvm 版本当中还会看到 val IO:CoroutineDispatcher,在 js 和 native 当中就只有前面提到的这三个了(对 Jvm 好偏心呐)。

|
|

Jvm

|

Js

|

Native

|
| — | — | — | — |
|

Default

|

线程池

|

主线程循环

|

主线程循环

|
|

Main

|

UI 线程

|

与 Default 相同

|

与 Default 相同

|
|

Unconfined

|

直接执行

|

直接执行

|

直接执行

|
|

IO

|

线程池

|

–

|

–

|

  • IO 仅在 Jvm 上有定义,它基于 Default 调度器背后的线程池,并实现了独立的队列和限制,因此协程调度器从 Default 切换到 IO 并不会触发线程切换。
  • Main 主要用于 UI 相关程序,在 Jvm 上包括 Swing、JavaFx、Android,可将协程调度到各自的 UI 线程上。
  • Js 本身就是单线程的事件循环,与 Jvm 上的 UI 程序比较类似。

3.2 编写 UI 相关程序

Kotlin 的用户绝大多数都是 Android 开发者,大家对 UI 的开发需求还是比较大的。我们举一个很常见的场景,点击一个按钮做点儿异步的操作再回调刷新 UI:

getUserBtn.setOnClickListener { 
    getUser { user ->
        handler.post {
            userNameView.text = user.name
        }
    }
}

我们简单得给出 getUser 函数的声明:

typealias Callback = (User) -> Unit

fun getUser(callback: Callback){
    ...
}

由于 getUser 函数需要切到其他线程执行,因此回调通常也会在这个非 UI 的线程中调用,所以为了确保 UI 正确被刷新,我们需要用 handler.post 切换到 UI 线程。上面的写法就是我们最古老的写法了。

后来又有了 RxJava,那么事情开始变得有趣了起来:

fun getUserObservable(): Observable<User> {
    return Observable.create<User> { emitter ->
        getUser {
            emitter.onNext(it)
        }
    }
}

于是点击按钮的事件可以这么写:

getUserBtn.setOnClickListener {
    getUserObservable()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { user ->
                userNameView.text = user.name
            }
}

其实 RxJava 在线程切换上的表现是非常优秀的,也正是如此,很多人甚至用它只是为了切线程方便!

那么我们现在把这段代码过渡到协程的写法:

suspend fun getUserCoroutine() = suspendCoroutine<User> {
    continuation ->
    getUser {
        continuation.resume(it)
    }
}

按钮点击时,我们可以:

getUserBtn.setOnClickListener {
    GlobalScope.launch(Dispatchers.Main) {
        userNameView.text = getUserCoroutine().name
    }
}

大家也可以用 anko-coroutines 当中的 View.onClick 扩展,这样我们就无需自己在这里用 launch 启动协程了。有关 Anko 对协程的支持,我们后面专门安排一篇文章介绍。

这里又有大家没见过的内容啦, suspendCoroutine 这个方法并不是帮我们启动协程的,它运行在协程当中并且帮我们获取到当前协程的 Continuation 实例,也就是拿到回调,方便后面我们调用它的 resume 或者 resumeWithException 来返回结果或者抛出异常。

如果你重复调用 resume 或者 resumeWithException 会收获一枚 IllegalStateException,仔细想想这是为什么。

对比前面的 RxJava 的做法,你会发现这段代码其实很容易理解,你甚至会发现协程的使用场景与 RxJava 竟是如此的相似。这里我们用到了 Dispatchers.Main 来确保 launch 启动的协程在调度时始终调度到 UI 线程,那么下面我们来看看 Dispatchers.Main 的具体实现。

在 Jvm 上, Main 的实现也比较有意思:

internal object MainDispatcherLoader {
    @JvmField
    val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()

    private fun loadMainDispatcher(): MainCoroutineDispatcher {
        return try {
            val factories = MainDispatcherFactory::class.java.let { clz ->
                ServiceLoader.load(clz, clz.classLoader).toList()
            }
            factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
                ?: MissingMainCoroutineDispatcher(null)
        } catch (e: Throwable) {
            MissingMainCoroutineDispatcher(e)
        }
    }
}

在 Android 当中,协程框架通过注册 AndroidDispatcherFactory 使得 Main 最终被赋值为 HandlerDispatcher 的实例,有兴趣的可以去看下 kotlinx-coroutines-android 的源码实现。

注意前面对于 RxJava 和协程的实现,我们都没有考虑异常和取消的问题。有关异常和取消的话题,我们会在后面的文章中详细介绍。

3.3 绑定到任意线程的调度器

调度器的目的就是切线程,你不要想着我在 dispatch 的时候根据自己的心情来随机调用,那你是在害你自己(不怕各位笑话,这样的代码我还真写过,仅供娱乐)。那么问题就简单了,我们只要提供线程,调度器就应该很方便的创建出来:

suspend fun main() {
    val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
    GlobalScope.launch(myDispatcher) {
        log(1)
    }.join()
    log(2)
}

输出的信息就表明协程运行在我们自己的线程上。

16:10:57:130 [MyThread] 1
16:10:57:136 [MyThread] 2

不过请大家注意,由于这个线程池是我们自己创建的,因此我们需要在合适的时候关闭它,不然的话:

我们可以通过主动关闭线程池或者调用:

myDispatcher.close()

来结束它的生命周期,再次运行程序就会正常退出了。

当然有人会说你创建的线程池的线程不是 daemon 的,所以主线程结束时 Jvm 不会停止运行。说的没错,但该释放的还是要及时释放,如果你只是在程序的整个生命周期当中短暂的用了一下这个调度器,那么一直不关闭它对应的线程池岂不是会有线程泄露吗?这就很尴尬了。

Kotlin 协程设计者也特别害怕大家注意不到这一点,还特地废弃了两个 API 并且开了一个 issue 说我们要重做这套 API,这两个可怜的家伙是谁呢?

废弃的两个基于线程池创建调度器的 API

fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher

这二者可以很方便的创建绑定到特定线程的调度器,但过于简洁的 API 似乎会让人忘记它的风险。Kotlin 一向不爱做这种不清不楚的事儿,所以您呢,还是像我们这一节例子当中那样自己去构造线程池吧,这样好歹自己忘了关闭也怨不着别人(哈哈哈)。

其实在多个线程上运行协程,线程总是这样切来切去其实并不会显得很轻量级,例如下面的例子就是比较可怕的了:

Executors.newFixedThreadPool(10)
        .asCoroutineDispatcher().use { dispatcher ->
            GlobalScope.launch(dispatcher) {
                log(1)
                val job = async {
                    log(2)
                    delay(1000)
                    log(3)
                    "Hello"
                }
                log(4)
                val result = job.await()
                log("5. $result")
            }.join()
            log(6)
        }

这里面除了 delay 那里有一次不可避免的线程切换外,其他几处协程挂起点的继续操作( Continuation.resume)都会切线程:

16:28:04:771 [pool-1-thread-1] 1
16:28:04:779 [pool-1-thread-1] 4
16:28:04:779 [pool-1-thread-2] 2
16:28:05:790 [pool-1-thread-3] 3
16:28:05:793 [pool-1-thread-4] 5. Hello
16:28:05:794 [pool-1-thread-4] 6

如果我们的线程池只开 1 个线程,那么这里所有的输出都将在这唯一的线程中打印:

16:40:14:685 [pool-1-thread-1] 1
16:40:14:706 [pool-1-thread-1] 4
16:40:14:710 [pool-1-thread-1] 2
16:40:15:723 [pool-1-thread-1] 3
16:40:15:725 [pool-1-thread-1] 5. Hello
16:40:15:725 [pool-1-thread-1] 6

对比这二者,10个线程的情况线程切换次数最少 3次,而 1 个线程的情况则只要 delay 1000ms 之后恢复执行的时候那一次。只是多两次线程切换,到底会有多大影响呢?我在我自己的 2015 款 mbp 上对于两种不同的情况分别循环运行 100 次,得到的平均时间如下:

|
线程数

|

10

|

1

|
| — | — | — |
|

耗时ms

|

1006.00

|

1004.97

|

注意,为了测试的公平性,在运行 100 次循环之前已经做好了预热,确保所有类都已经加载。测试结果仅供参考。

也就是说多两次线程切换平均能多出 1ms 的耗时。生产环境当中的代码当然会更复杂,如果这样用线程池去调度,结果可想而知。

实际上通常我们只需要在一个线程当中处理自己的业务逻辑,只有一些耗时的 IO 才需要切换到 IO 线程中处理,所以好的做法可以参考 UI 对应的调度器,自己通过线程池定义调度器的做法本身没什么问题,但最好只用一个线程,因为多线程除了前面说的线程切换的开销外,还有线程安全的问题。

3.4 线程安全问题

Js 和 Native 的并发模型与 Jvm 不同,Jvm 暴露了线程 API 给用户,这也使得协程的调度可以由用户更灵活的选择。越多的自由,意味着越多的代价,我们在 Jvm 上面编写协程代码时需要明白一点的是,线程安全问题在调度器不同的协程之间仍然存在。

好的做法,就像我们前面一节提到的,尽量把自己的逻辑控制在一个线程之内,这样一方面节省了线程切换的开销,另一方面还可以避免线程安全问题,两全其美。

如果大家在协程代码中使用锁之类的并发工具就反而增加了代码的复杂度,对此我的建议是大家在编写协程代码时尽量避免对外部作用域的可变变量进行引用,尽量使用参数传递而非对全局变量进行引用。

以下是一个错误的例子,大家很容易就能想明白:

suspend fun main(){
    var i = 0
    Executors.newFixedThreadPool(10)
            .asCoroutineDispatcher().use { dispatcher ->
                List(1000000) {
                    GlobalScope.launch(dispatcher) {
                        i++
                    }
                }.forEach {
                    it.join()
                }
            }
    log(i)
}

输出的结果:

16:59:28:080 [main] 999593

4. suspend main 函数如何调度?

上一篇文章我们提到了 suspend main 会启动一个协程,我们示例中的协程都是它的子协程,可是这个最外层的协程到底是怎么来的呢?

我们先给出一个例子:

suspend fun main() {
    log(1)
    GlobalScope.launch {
        log(2)
    }.join()
    log(3)
}

它等价于下面的写法:

fun main() {
    runSuspend {
        log(1)
        GlobalScope.launch {
            log(2)
        }.join()
        log(3)
    }
}

那你说这个 runSuspend 又是何妨神圣?它是 Kotlin 标准库的一个方法,注意它不是 kotlinx.coroutines 当中的,它实际上属于更底层的 API 了。

internal fun runSuspend(block: suspend () -> Unit) {
    val run = RunSuspend()
    block.startCoroutine(run)
    run.await()
}

而这里面的 RunSuspend 则是 Continuation 的实现:

private class RunSuspend : Continuation<Unit> {
    override val context: CoroutineContext
        get() = EmptyCoroutineContext

    var result: Result<Unit>? = null

    override fun resumeWith(result: Result<Unit>) = synchronized(this) {
        this.result = result
        (this as Object).notifyAll()
    }

    fun await() = synchronized(this) {
        while (true) {
            when (val result = this.result) {
                null -> (this as Object).wait()
                else -> {
                    result.getOrThrow() // throw up failure
                    return
                }
            }
        }
    }
}

它的上下文是空的,因此 suspend main 启动的协程并不会有任何调度行为。

通过这个例子我们可以知道,实际上启动一个协程只需要有一个 lambda 表达式就可以了,想当年 Kotlin 1.1 刚发布的时候,我写了一系列的教程都是以标准库 API 为基础的,后来发现标准库的 API 也许真的不是给我们用的,所以看看就好。

上述代码在标准库当中被修饰为 internal,因此我们无法直接使用它们。不过你可以把 RunSuspend.kt 当中的内容复制到你的工程当中,这样你就可以直接使用啦,其中的 varresult:Result<Unit>?=null 可能会报错,没关系,改成 privatevarresult:Result<Unit>?=null 就可以了。

5. 小结

在这篇文章当中,我们介绍了协程上下文,介绍了拦截器,进而最终引出了我们的调度器,截止目前,我们还有异常处理、协程取消、Anko 对协程的支持等话题没有讲到,如果大家有协程相关想了解的话题,可以留言哈~

更多的Kotlin学习资料可以扫码免费领取!

《Kotlin入门教程指南》

第一章 Kotlin 入门教程指南

​ ● 前言

img

第二章 概述

​ ● 使用 Kotlin 进行服务器端开发

​ ● 使用 Kotlin 进行 Android 开发

​ ● Kotlin JavaScript 概述

​ ● Kotlin/Native 用于原生开发

​ ● 用于异步编程等场景的协程

​ ● Kotlin 1.1 的新特性

​ ● Kotlin 1.2 的新特性

​ ● Kotlin 1.3 的新特性

img

第三章 开始

​ ● 基本语法

​ ● 习惯用法

​ ● 编码规范

img

第四章 基础

​ ● 基本类型

​ ● 包

​ ● 控制流:if、when、for、while

​ ● 返回和跳转

img

第五章 类与对象

​ ● 类与继承

​ ● 属性与字段

​ ● 接口

​ ● 可见性修饰符

​ ● 扩展

​ ● 数据类

​ ● 密封类

​ ● 泛型

​ ● 嵌套类与内部类

​ ● 枚举类

​ ● 对象表达式与对象声明

​ ● Inline classes

​ ● 委托

委托属性

img

安卓原生开发为何是初创企业的生存基础设施 安卓原生开发是移动应用开发的基础技术路径,指基于Android SDK、直接调用系统API构建高性能、高可控性客户端的能力。其核心原理在于绕过Web容器或跨端中间层,实现对硬件传感器、后台服务、离线存储及厂商推送等底层能力的稳定访问。相比H5、小程序或跨平台方案,原生开发在弱网适配、消息送达率、启动性能和隐私合规等方面具备不可替代的技术价值。典型应用场景包括社区团购实时订单同步、智能硬件蓝牙联动、车载Android Auto集成、金融类App安全保活等强交互、低延迟、高可靠性需求领域。对于资源有限的初创企业 阅读详情

相关推荐

JetBrains 2025版:AI原生IDE的底层重构与开发范式升级

现代集成开发环境(IDE)已从代码编辑工具演进为AI驱动的智能开发操作系统。其核心原理在于将大模型推理能力深度嵌入平台内核,构建符号图谱索引、上下文感知引擎与多语言AI任务流水线。这种AI原生架构显著提升代码生成、智能调试、文档解析与资源管理的技术价值,广泛应用于Java微服务、PHP协程框架(如Webman)、Ruby元编程及.NET MAUI跨平台开发等复杂场景。尤其在JetBrains 2025系列中,IntelliJ Platform首次实现AI能力标准化抽象(ai-engine层)与硬件级授权验证

weixin_30797199的博客 338

Kotlin协程之一(协程概念)

协程(Coroutine)的背景知识和基本的简单介绍。

u012878270的博客 238

2026年AI编程工具选型决策地图:本地化、上下文与安全的工程实践指南

AI编程工具已从通用代码补全演进为深度耦合开发环境的技术基础设施。其核心原理在于对IDE抽象语法树(AST)、构建系统语义(如Maven依赖图、Cargo feature开关)及运行时上下文(如Spring Boot配置、Rust所有权模型)的精准建模;技术价值体现在将调试耗时压缩至秒级、降低跨语言/跨版本迁移门槛,并保障私有代码零出域;典型应用场景覆盖遗留系统理解、本地化实时纠错、结对式重构意图实现等真实工作流;而2026年工程落地的关键,正取决于能否在Ollama本地推理、GitHub Copilot

weixin_33725239的博客 400

Kotlin协程:续体、续体拦截器、调度器

为什么协程需要EventLoop呢?协程的本质是续体传递,而续体传递的本质是回调,假设在Dispatchers.Unconfined调度下,要连续执行多个suspend方法,就会有多个续体传递,假设suspend方法达到一定数量后,就会造成StackOverflow,进而引起崩溃。同样的,我们知道调用runBlocking会阻塞当前线程,而runBlocking阻塞的原理就是执行“死循环”,因此需要在循环中做任务的分发,去执行内部协程在Dispatchers.Unconfined调度器下加入的任务。...

LeeDuoZuiShuai的博客 942

IntelliJ IDEA 2025.1安装机制深度解析:JDK发现、Runtime嵌入与跨平台适配

IntelliJ IDEA 是一款面向Java等JVM语言的集成开发环境(IDE),其安装过程涉及JDK环境识别、运行时(Runtime)配置与多平台兼容性等核心机制。2025.1版本重构了底层安装器,引入Runtime-Embedded JDK Discovery机制,不再依赖PATH或JAVA_HOME,而是通过签名验证扫描标准路径下的JDK;同时统一为单安装包架构,并强制集成JetBrains Runtime(JBR)。这一变化直接影响开发者环境配置、自动化部署脚本及CI/CD流水线稳定性。在Wind

weixin_33739627的博客 533

Android+PHP+MySQL登录注册系统实战指南

用户认证是移动应用开发的基础能力,其核心在于前后端安全、稳定、可调试的HTTP交互。理解HTTP请求响应机制、密码加盐哈希原理、SQL注入防护手段,是构建可靠登录系统的前提。技术价值体现在轻量可控——无需复杂框架即可实现跨安卓机型兼容、HTTPS通信、数据库防注入与字段级数据校验。典型应用场景包括教育类App、本地化工具软件等对高并发无要求但强调快速上线与自主运维的项目。本文聚焦Android原生客户端、PHP原生后端与MySQL数据库协同落地的真实链路,覆盖OkHttp网络调度、PDO预处理、utf8mb

weixin_34162228的博客 394

超长文,带你全面了解Kotlin的协程

/ 今日科技快讯 /工信部近日发布了2019年1-11月通信业经济运行情况。数据显示,截至11月底,三家基础电信企业的移动电话用户总数达16亿户,同比增长2.7%,较上年末净增3...

郭霖 2817

【信息科学与工程学】【数据科学】数据科学领域 第六篇 算法设计 07 堆、栈、表、矩阵、集合、队列、数组、字符串、树、图、动态规划、结构体、元组的算法01

当前段必须包含所有字符的最后出现位置。1. 字符串分割成最多片段2. 基因序列功能域划分3. 文本段落划分4. 音频声道分离5. 图像色彩分区6. 社交网络社群划分7. 蛋白质结构域8. 音乐乐章分段9. 代码模块划分10. 章节划分特征:一次扫描,贪心确定边界。

weixin_49199313的博客 1276

kotlin语法进阶 - 协程(一)协程基础

kotlin协程基础

weixin_43864176的博客 4162

【信息科学与工程学】计算机科学与自动化——第三百零四篇 高性能系统开发指南01

高并发:限流、熔断、降级高性能:红包拆分、多级缓存、大Key/热Key拆分、Pipeline、线程池、RocketMQ零丢失、幂等、分布式锁、集群缩容、分批入库海量数据处理:日志分表分片、跨表查询、冷热分层、大数据引擎服务器选型系统安全:网关安全、服务器安全、云存储安全、风控、监控、测试数据一致性:缓存与数据库一致性、大厂方案、蓝绿回滚、流量一致性、流程一致性项目与团队管理:流程问题、管理问题(风险、成本、人员、文档、培训)稳定性建设:上线三板斧、线上五步闭环、混沌工程、容量规划。

weixin_49199313的博客 251

飞桨轻量AI虚拟主播:Python本地部署语音-唇形同步方案

虚拟主播系统本质是语音合成与面部动画的跨模态对齐技术,其核心原理在于声学特征(如梅尔频谱)到视觉参数(如唇部关键点)的实时映射。飞桨(PaddlePaddle)凭借PaddleSpeech和PaddleGAN两大套件,在端侧推理、模型压缩与动态图部署方面展现出独特技术优势,显著降低硬件门槛。相比PyTorch生态,飞桨在CPU/GPU混合调度、静态图校验、ONNX导出鲁棒性及中文语音前端深度适配等方面具备工程级落地价值。该方案广泛适用于教育本地化直播、中小企业数字人开发、家庭智能交互等对隐私性、低延迟和离线

weixin_42528090的博客 263

阿里P7大佬教你破解 Kotlin 协程(5) - 协程取消篇

​ ● 协程是什么​ ● 什么是Job 、Deferred 、协程作用域​ ● Kotlin协程的基础用法。

Android_XG的博客 535

保姆级教程:手把手调试PX4Ctrl的油门映射与姿态控制(附ROS节点配置)

本文提供PX4Ctrl的深度调参指南,涵盖油门映射、姿态控制及ROS节点配置等关键环节。通过详细解析PX4控制器的核心逻辑、递归最小二乘法(RLS)参数估计及状态机设计,帮助开发者优化无人机飞控性能。附实战案例与安全调试建议,适合PX4开发者进阶学习。

weixin_30363981的博客 382

工作中使用到的单词(软件开发)

1.エイリアス   alias 别名 powershell使用 get-help 命令时,会得到与此命令 相关(别名)的命令 2.プロキシ proxy 代 理 3.プロモート   promote 促进,提升   开发的系统新功能上线发布,有时会使用到这个词,比如 STプロモート 还有使用リリース这个词的。比如STリリース。个人认为两者含义相同。 4.見逃し み................................................................

sun0322 6万+

react-window 大数据列表和表格数据渲染组件之虚拟滚动

ReactWindow是一个高性能的React虚拟滚动组件库,专门用于高效渲染大型列表和表格数据。它通过“窗口化”技术,仅渲染用户可见区域的内容,大幅提升性能并减少内存消耗。主要特点包括:支持固定/可变大小列表项、多种布局类型(垂直/水平/网格)、轻量级设计(不足5KB)以及平滑的滚动体验。相比传统渲染方式,它能轻松处理成千上万条数据,并提供自动滚动、自适应容器大小等高级功能。与同类库相比,ReactWindow具有更简洁的API和更优的性能表现,是处理大数据量渲染场景的理想选择。

前端开发-前端技术分享 1137

STM32鱼塘水质监测系统设计与实现

物联网技术在农业领域的应用正逐步深入,其中传感器网络作为关键基础设施,通过实时数据采集与传输实现环境监测。基于STM32微控制器的嵌入式系统因其高性能和低功耗特性,成为物联网终端设备的理想选择。该系统通过DS18B20水温传感器、PH传感器和浊度传感器组成的多参数检测网络,配合4G通信模块实现水质数据的远程监控。在农业物联网场景中,这种解决方案不仅大幅降低了传统人工检测的成本,更能通过实时预警有效预防养殖损失。本文介绍的鱼塘水质监测系统正是这一技术的典型应用,展示了如何通过STM32和常见传感器构建高性价比

weixin_42173218的博客 621

【深入理解Kotlin协程】协程的上下文 CoroutineContext

​ CoroutineContext使用以下元素集定义协程的行为: Job:控制协程的生命周期。 CoroutineDispatcher:将工作分派到适当的线程。 CoroutineName:协程的名称,可用于调试。 CoroutineExceptionHandler:处理未捕获的异常。 launch、async、runBlocking 等启动协程方式第一个参数都是需要传入一个协程上下文,当然默认的都是EmptyCoroutineContext。

川峰的博客 957

python 抖音采集_python爬取抖音视频的实例详解

import requestsimport jsonimport reimport osfrom pprint import pprint as ppimport queueclass DouYin:header = {'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*...

weixin_35795675的博客 1864

破解 Kotlin 协程(3) - 协程调度篇

上一篇我们知道了协程启动的几种模式,也通过示例认识了 `launch` 启动协程的使用方法,本文将延续这些内容从调度的角度来进一步为大家揭示协程的奥义。

Kotlinlang 2161

【项目实战】Java9的新特性模块化(Module)

Java 9引入了模块化系统,称为"Java Platform Module System"(JPMS)这个系统允许将Java程序分成模块,每个模块都有自己的规范,可以明确地声明它依赖于哪些其他模块,并且只能访问被其所依赖的模块暴露的API。这样可以模块化就是增加了更高级别的聚合,是Package的封装体。Package是一些类路径名字的约定,而模块是一个或多个Package组成的封装体。

奶爸的编程之路,也就一周冷个三天 1897

基于环形槽谐振器与PIN二极管的频率可重构Vivaldi天线设计与CST仿真研究

摘要 本文设计了一种基于FR4基板的频率可重构Vivaldi天线,通过集成环形槽谐振器(CSRR)和PIN二极管开关实现超宽带(UWB,2-8 GHz)与窄带(2.4/3.5/5.2 GHz)模式的电控切换。天线采用指数渐变槽线结构,结合CSRR的滤波特性和PIN二极管的开关功能,利用CST仿真优化参数。结果表明,UWB模式下S11<-10 dB,窄带模式下目标频点谐振显著,其他频段抑制良好,验证了设计的可行性。该方案为多频段无线通信系统提供了紧凑、低成本的可重构天线解决方案。 关键词:频率可重构天线

huanghm88的专栏 219

Kotlin协程(同步、异步和常用的API)

一、什么是协程 1.协作程序,解决异步问题 2.应用层完成调度 二、协程要解决的问题 1.看下伪代码 现在开始我们要运行协程啦{ //现在我们在协程里面 //花括号内的代码可以是一段运行在UI线程的代码,比如Android初始化UI val 视图 = ...//初始化视图 //注意 图片 = 异步加载图片(图片的地址) 视图.显示图片(图片) ...

向小凯同学学习 6169

D457深度相机调试记录(max96712+max9295A)

使用max96712解串器解4路D457相机。采集深度相机Depth和RGB流。

navy2015_163的博客 1269

Android开发者快速上手Kotlin(六) 之 协程上下文和拦截器

接《Android开发者快速上手Kotlin(五) 之 初只协程》文章继续。 12 协程进阶 我们在完成了上一篇文章的学习后,你是不是已经大概清楚协程是什么和怎样使用了。但是你可能还存在着疑惑,既然协程没有异常的能力,使用过程中还需要自己去创建线程,那为什么不直接使用线程直截了当,还要绕一个大圈写那么多语法来完成一个看似高端却没什么实际意义的玩意?其实大多数人在初学习协程时都会存在这样的疑问,...

子云心的博客 1398

基于 Qt C++ 的智能工厂数字孪生系统

你想要实现一个基于 Qt C++ 的智能工厂数字孪生系统,核心是 **Qt 3D** 场景搭建 + **实时数据驱动** 设备模型。下面我将为你提供一个完整的解决方案,包含 **核心思路、实现步骤、完整代码示例** 和 **注意事项**。| **渲染层** | 3D 场景搭建、设备模型加载、实时动画渲染、用户交互。| **业务逻辑层** | 数据处理(滤波、转换)、设备状态管理、动画逻辑控制。| **数据层** | 实时采集设备数据(如传感器、PLC、MES 系统),提供数据接口。

寒寒豆豆软件公司的博客 195

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain&#39;s apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath(&#39;DeepLearnToolbox&#39;));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand(&#39;state&#39;,0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W&#39;); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand(&#39;state&#39;,0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = &#39;sigm&#39;;%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, &#39;Too big error&#39;);```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand(&#39;state&#39;,0)sae = saesetup([784 100]);sae.ae{1}.activation_function = &#39;sigm&#39;;sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)&#39;)% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = &#39;sigm&#39;;nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, &#39;Too big error&#39;);```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x&#39;,28,28,60000))/255;test_x = double(reshape(test_x&#39;,28,28,10000))/255;train_y = double(train_y&#39;);test_y = double(test_y&#39;);%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you&#39;ll get around 1.2% errorrand(&#39;state&#39;,0)cnn.layers = { struct(&#39;type&#39;, &#39;i&#39;) %input layer struct(&#39;type&#39;, &#39;c&#39;, &#39;outputmaps&#39;, 6, &#39;kernelsize&#39;, 5) %convolution layer struct(&#39;type&#39;, &#39;s&#39;, &#39;scale&#39;, 2) %sub sampling layer struct(&#39;type&#39;, &#39;c&#39;, &#39;outputmaps&#39;, 12, &#39;kernelsize&#39;, 5) %convolution layer struct(&#39;type&#39;, &#39;s&#39;, &#39;scale&#39;, 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, &#39;Too big error&#39;);```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand(&#39;state&#39;,0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, &#39;Too big error&#39;);%% ex2 neural net with L2 weight decayrand(&#39;state&#39;,0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, &#39;Too big error&#39;);%% ex3 neural net with dropoutrand(&#39;state&#39;,0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, &#39;Too big error&#39;);%% ex4 neural net with sigmoid activation functionrand(&#39;state&#39;,0)nn = nnsetup([784 100 10]);nn.activation_function = &#39;sigm&#39;; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, &#39;Too big error&#39;);%% ex5 plotting functionalityrand(&#39;state&#39;,0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = &#39;softmax&#39;; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, &#39;Too big error&#39;);%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand(&#39;state&#39;,0)nn = nnsetup([784 20 10]); nn.output = &#39;softmax&#39;; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, &#39;Too big error&#39;);```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

Kotlin协程源码分析-8 拦截器

前言 协程拦截器ContinuationInterceptor作用: 线程切换 hook结果(一般用不到) 我们开发Swing或者Android更新UI需要在特定的线程上进行操作,那么这里就涉及线程的切换问题,那么我们看看ContinuationInterceptor在本章中如何使用和源码分析。 简单案例 class MyContinuation() : Continuation<St...

不会写代码的丝丽 2039

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

上一篇: Android程序员的悲哀是什么?
下一篇: 华为“不造车”,只大力研发车载OTA,车载OTA有何魅力
不秃头de程序猿
博客等级 码龄5年 4089粉丝 1478原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值