【问题标题】:Kotlin Coroutines Async Await SequenceKotlin 协程异步等待序列
【发布时间】:2019-02-21 11:34:59
【问题描述】:

你能解释一下这两个代码块有什么区别吗?第一次打印 421,但第二次打印 606。为什么第一个是并行的,第二个是顺序的?

fun main(args: Array<String>) = runBlocking {

    var time = measureTimeMillis {
        val one = async { one() }
        val two = async { two() }
        val int1 = one.await()
        val int2 = two.await()
        println(int1 + int2)

    }

    println(time)


    time = measureTimeMillis {
        val one = async { one() }.await()
        val two = async { two() }.await()
        println(one + two)

    }

    print(time)
}

suspend fun one(): Int {
    delay(200)
    return 12
}

suspend fun two(): Int {
    delay(400)
    return 23
}

【问题讨论】:

  • 我不确定您如何获得这些值中的任何一个。 12 + 23 = 35。你究竟如何得到 421 和 606 超出了我的理解。也许确认您在问题中输入的内容,然后我会看看是否可以提供帮助。异步等待通常替换为“withContext(yourProviderThread){} 然后不需要等待,但无论哪种方式 async { someMethod }.await() 都是可以接受的,如果你不等待排队,那么你可以轻松做到one.await() + two.await() 这也应该给出相同的值。但是当你的输出不对齐时很难推测。
  • 421/606 是运行变体 1 和变体 2 所需的毫秒数 ;-)

标签: kotlin coroutine kotlin-coroutines


【解决方案1】:

拇指规则:

  • 当您不需要并行执行时,请使用 withContext
  • 仅在需要并行执行时使用 asyncwithContextasync 都可以用来获得启动时无法获得的结果。
  • 使用 withContext 返回单个任务的结果。'
  • 对并行运行的多个任务的结果使用异步。

查看this了解更多详情

【讨论】:

    【解决方案2】:

    在第一个变体中,两个异步调用都会得到一个Deferred&lt;Int&gt;。正如the documentation of Deferred 很好地展示了延迟对象可能处于的几种状态。从外部来看,该状态现在是newactive,但肯定还没有完成。但是,在您的第二个变体中,第一个 async-await 已经需要 completed 状态,否则您将无法获得任何价值。但是,在您的 async{one()}.await() 上,第二个 async 尚不清楚。 还要注意await() 的返回值现在是Int 而不是Deferred,所以此时协程肯定已经执行了。还要检查the documentation of await()

    换句话说:

    val one = async { one() }
    val two = async { two() }
    

    onetwo 现在都是 Deferred&lt;Int&gt;。还没有被调用(或者可能已经被调用)。只要您调用one.await(),它可能已经同时启动onetwo,只是因为它有它的资源(即使您没有在代码中的任何地方使用two.await())。

    然而,关于第二个变体:

    val one = async { one() }.await()
    val two = async { two() }.await()
    

    即使它为async {one()} 创建了一个协程,它也必须立即将一个值设置为one,因为您正在调用await()onetwo的类型都是Int。因此,一旦命中这些行中的第一行,就需要立即执行异步代码。到那时,没有人知道在我们等待第一个异步调用的值时必须执行另一个异步调用。如果第一个没有await,协程将再次并行执行,例如:

    val one = async { one() }
    val two = async { two() }.await()
    

    将并行执行one()two()

    所以也许这可以概括为:只有那些协同程序可以在等待时并行执行,它们是当时已知/产生的。

    【讨论】:

    • 这并不重要知道;更多的是关于你告诉。在第二个变体中,您告诉编译器在继续之前等待第一个协程的执行(在创建第二个协程之前)。 await 是一个非挂起函数。
    • await其实是一个挂起函数。
    • 好的...“知道”可能是错误的词...一个协程不知道另一个协程...只是想说类似的话...例如所有产生的协程(我更喜欢这个公式)都被“协程执行器”知道,因此可以在第一次等待被命中时运行......这对于那时还不知道的协程不起作用。 ..例如在等待之后初始化的...
    【解决方案3】:
    val one = async { one() }
    val two = async { two() }
    val int1 = one.await()
    val int2 = two.await()
    

    这是做什么的:

    1. 生成任务一
    2. 生成任务二
    3. 等待任务一
    4. 等待任务二

    val one = async { one() }.await()
    val two = async { two() }.await()
    

    这是做什么的:

    1. 生成任务一
    2. 等待任务一
    3. 生成任务二
    4. 等待任务二

    这里没有并发,它是纯粹的顺序代码。事实上,对于顺序执行,您甚至不应该使用async。正确的成语是

    val one = withContext(Dispatchers.Default) { one() }
    val two = withContext(Dispatchers.Default) { two() }
    

    【讨论】:

    • withContextsn-p 和简单的val one = one(); val two = two() 有什么区别?
    • 我们假设one()two() 是阻塞函数并且调用线程有一个事件循环(最典型的是,它是UI 线程)。只写one() 会阻塞当前线程,写withContext(Default) { one() } 会将阻塞调用转移到线程池,而当前线程可以继续处理其他事件,直到withContext() 的结果准备好。
    • 请注意,即使代码是按顺序执行的,它也不会一直在同一个线程上执行。像这样在线程之间跳转的协程非常类似于在 CPU 内核之间跳转的线程。线程之于协程就像 CPU 内核之于线程(模抢占式调度)。
    【解决方案4】:

    在 Marko Topolnik 的回答之后,我尝试了不同的变体,我认为它被接受了。但是一件有趣的事情是,如果我启动协程而不调用 await,函数会启动但不会结束。下面是我的代码。

    fun main(args: Array<String>) = runBlocking {
    
        var time = measureTimeMillis {
            val one = async { one(1) }
            val two = async { two(1) }
            val int1 = one.await()
            val int2 = two.await()
        }
    
        println("time: $time")
    
    
        time = measureTimeMillis {
            val one = async { one(2) }.await()
            val two = async { two(2) }.await()
        }
    
        println("time: $time")
    
        time = measureTimeMillis {
            val one = async { one(3) }
            val two = async { two(3) }
        }
    
        println("time: $time")
    
    
    
    }
    
    suspend fun one(iteration: Int): Int {
        println("func1 start, iteration $iteration")
        delay(200)
        println("func1 end, iteration $iteration")
        return 12
    }
    
    suspend fun two(iteration: Int): Int {
        println("func2 start, iteration $iteration")
        delay(400)
        println("func2 end, iteration $iteration")
        return 23
    }
    

    输出是,

    func1 开始,迭代 1
    func2 开始,迭代 1
    func1 结束,迭代 1
    func2 结束,迭代 1
    时间:430
    func1 开始,迭代 2
    func1 结束,迭代 2
    func2 开始,迭代 2
    func2 结束,迭代 2
    时间:607
    func1 开始,迭代 3
    时间:2
    func2 开始,迭代 3

    进程以退出代码 0 结束

    【讨论】:

    • 当你写measureTimeMillis { async { one(3) }; async { two(3) } }时,你允许你的主线程在异步任务完成之前完成。一旦它死了,JVM中就没有更多的非守护线程了,所以当任务仍在CommonPool上运行时它就会退出。
    猜你喜欢
    • 2022-01-18
    • 1970-01-01
    • 2021-10-27
    • 1970-01-01
    • 2020-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-29
    相关资源
    最近更新 更多