在 Kotlin 协程中,launch/join 和 async/await 有什么区别?

273

kotlinx.coroutines 库中,您可以使用 launch(带有join)或 async(带有await)启动新的协程。它们之间有什么区别?

8个回答

380
  • launch用于启动一个协程,类似于启动新线程。如果launch内部的代码以异常终止,则会像线程中未捕获的异常一样处理--通常在后端JVM应用程序中打印到stderr并导致Android应用程序崩溃。join用于等待启动的协程完成,它不会传播其异常。但是,崩溃的子协程也会使用相应的异常取消其父协程。

  • async用于启动一个计算某个结果的协程。结果由Deferred的实例表示,并且您必须对其使用await。在async代码内部发生的未捕获异常将存储在生成的Deferred中,并且不会传递到其他任何地方,除非进行处理,否则它将被静默丢弃。您不得忘记使用async启动的协程。


2
在Android中,异步是否是正确的协程构建器来处理网络调用? - Faraaz
14
你能详细说明一下“你必须不要忘记关于使用async开始的协程”吗?是否存在一些出乎意料的注意事项呢? - Luis
4
异步代码中的未捕获异常会被存储在生成的Deferred对象中,并且不会传递到其他地方,除非被处理,否则它将被悄悄地丢弃。 - Roman Elizarov
26
如果您忘记异步操作的结果,那么它将完成并被垃圾回收。但是,如果由于代码中的某些错误导致其崩溃,则您将永远不会得知。这就是为什么。 - Roman Elizarov
3
@RomanElizarov 我认为异常处理的描述现在已经过时了?是否可以更新一下,因为当你搜索协程异常处理时,它仍然排名很高。或者告诉我我错了吗 :-) ? - matt freake
显示剩余14条评论

137

我发现这个指南很有用。我将引用其中的关键部分。

协程

本质上,协程是轻量级线程。

因此,您可以将协程视为以非常高效的方式管理线程的东西。

launch

fun main(args: Array<String>) {
    launch { // launch new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

launch 启动协程,执行一些操作,并立即返回一个作为 Job 的标记。您可以在此 Job 上调用 join 来阻塞,直到此 launch 协程完成。

fun main(args: Array<String>) = runBlocking<Unit> {
    val job = launch { // launch new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
}

异步操作(async)

async从概念上来说就像是launch。它启动了一个独立的协程,这是一个轻量级的线程,可以与所有其他协程并发地工作。不同之处在于,launch返回一个Job但不带任何结果值,而async则返回一个Deferred——一个轻量级的非阻塞future对象,表示承诺稍后提供结果。

因此,async启动一个后台线程,执行某些操作,并立即返回一个Deferred标记。

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}
你可以使用.await()获取deferred value的最终结果,但是Deferred也是一个Job,所以如果需要,你可以取消它。
因此,Deferred实际上是一个Job。查看此文档以了解更多详情。
interface Deferred<out T> : Job (source)

async 默认情况下是急切的

使用可选的 start 参数,并将其值设置为 CoroutineStart.LAZY,可以提供 async 的惰性选项。仅当某些 await 需要该协程的结果或调用启动函数时,才会启动协程。


1
在使用 runBlocking 内的 launch 时,代码块示例中不需要 "job.join()",因为 runBlocking 协程会等待其子协程完成。只有在使用顶级作用域(例如 GlobalScope)创建协程时才需要这样做。 - Avilio
@Avilio,这不会有什么影响,虽然在这个例子中调用join()没有意义。另外一件事:launch示例根本无法编译(launch需要CoroutineScope)。 - geiger

18
  1. launch和async这两个协程构建器本质上都是带有CoroutineScope接收者的lambda表达式,这意味着它们的内部块被编译为挂起函数,因此它们都以异步模式运行,并且它们都会按顺序执行其块。

  2. launch和async的区别在于它们提供了两种不同的可能性。launch构建器返回一个Job,而async函数将返回一个Deferred对象。你可以使用launch来执行一个你不希望从中返回任何值的块,例如写入数据库、保存文件或处理一些数据,基本上只是为了产生副作用。另一方面,正如我之前所述,返回Deferred的async将从其块的执行中返回一个有用的值,一个包装你的数据的对象,因此你主要可以使用它的结果,但也可能使用它的副作用。注意:你可以使用函数await来剥离Deferred并获取其值,这将阻塞你的语句执行,直到返回一个值或抛出异常!你可以通过使用函数join()来使用launch获得相同的效果。

  3. launch和async这两个协程构建器都是可取消的。

  • 还有什么需要吗?:是的,如果在其块内抛出异常,则协程将自动取消并传递异常。另一方面,如果发生这种情况,则异步异常不会进一步传播,并且应该在返回的Deferred对象中被捕获/处理。

  • 更多关于协程的信息:https://kotlinlang.org/docs/tutorials/coroutines/coroutines-basic-jvm.html https://www.codementor.io/blog/kotlin-coroutines-6n53p8cbn1


  • 1
    谢谢您的评论。它收集了该主题的所有要点。 我想补充一点,不是所有的启动都可以取消,例如原子启动永远不能被取消。 - alexanderktx

    17

    launchasync用于启动新的协程。但是,它们以不同的方式执行。

    我想展示一个非常基本的例子,可以帮助你很容易地理解区别。

    1. launch
        class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            btnCount.setOnClickListener {
                pgBar.visibility = View.VISIBLE
                CoroutineScope(Dispatchers.Main).launch {
                    val currentMillis = System.currentTimeMillis()
                    val retVal1 = downloadTask1()
                    val retVal2 = downloadTask2()
                    val retVal3 = downloadTask3()
                    Toast.makeText(applicationContext, "All tasks downloaded! ${retVal1}, ${retVal2}, ${retVal3} in ${(System.currentTimeMillis() - currentMillis)/1000} seconds", Toast.LENGTH_LONG).show();
                    pgBar.visibility = View.GONE
                }
            }
    
        // Task 1 will take 5 seconds to complete download
        private suspend fun downloadTask1() : String {
            kotlinx.coroutines.delay(5000);
            return "Complete";
        }
    
        // Task 1 will take 8 seconds to complete download    
        private suspend fun downloadTask2() : Int {
            kotlinx.coroutines.delay(8000);
            return 100;
        }
    
        // Task 1 will take 5 seconds to complete download
        private suspend fun downloadTask3() : Float {
            kotlinx.coroutines.delay(5000);
            return 4.0f;
        }
    }
    

    在这个例子中,我的代码会在点击 btnCount 按钮时下载 3 个数据,并在所有下载完成之前显示 pgBar 进度条。有三个 suspend 函数 downloadTask1()downloadTask2()downloadTask3(),用于下载数据。为了模拟它,我在这些函数中使用了 delay()。这些函数分别等待 5 秒8 秒5 秒
    由于我们使用了 launch 来启动这些挂起函数,launch 将按顺序(一个接一个)执行它们。这意味着,在 downloadTask1() 完成后,downloadTask2() 将开始,只有在 downloadTask2() 完成后,downloadTask3() 才会开始。
    如输出截图中的 Toast 所示,使用 launch 完成所有 3 次下载所需的总执行时间将导致 5 秒 + 8 秒 + 5 秒 = 18 秒

    Launch Example

    正如我们所看到的,launch 使得所有三个任务都按顺序执行。完成所有任务的时间为 18 秒
    如果这些任务是独立的,而且它们不需要其他任务的计算结果,我们可以让它们在后台并发运行。它们将同时启动并并发运行。可以使用 async 来实现这一点。 async 返回一个 Deffered<T> 类型的实例,其中 T 是我们的挂起函数返回的数据类型。例如,
    • downloadTask1() 将返回 Deferred<String>,因为 String 是函数的返回类型
    • downloadTask2() 将返回 Deferred<Int>,因为 Int 是函数的返回类型
    • downloadTask3() 将返回 Deferred<Float>,因为 Float 是函数的返回类型
    我们可以使用 async 的返回对象,类型为 Deferred<T>,来获取返回值,类型为 T。可以使用 await() 调用来实现。请查看下面的代码示例。
            btnCount.setOnClickListener {
            pgBar.visibility = View.VISIBLE
    
            CoroutineScope(Dispatchers.Main).launch {
                val currentMillis = System.currentTimeMillis()
                val retVal1 = async(Dispatchers.IO) { downloadTask1() }
                val retVal2 = async(Dispatchers.IO) { downloadTask2() }
                val retVal3 = async(Dispatchers.IO) { downloadTask3() }
    
                Toast.makeText(applicationContext, "All tasks downloaded! ${retVal1.await()}, ${retVal2.await()}, ${retVal3.await()} in ${(System.currentTimeMillis() - currentMillis)/1000} seconds", Toast.LENGTH_LONG).show();
                pgBar.visibility = View.GONE
            }
    

    这样,我们同时启动了所有3个任务。因此,我的总执行时间将仅为8秒,这是downloadTask2()的时间,因为它是3个任务中最大的。您可以在以下截图中的Toast message中看到这一点。

    await example


    14
    谢谢您提到launch是用于顺序函数,而async用于并发函数。 - Akbolat SSS
    4
    您已经使用launch一次性启动所有任务,而对于每个任务都使用了async。可能之所以更快是因为每个任务都在另一个协程中启动并且不需要等待其他任务?但这是一个错误的比较。通常性能是相同的。一个关键的区别是,launch总是启动一个新的协程,而async会分割原有的协程。另一个因素是,如果其中一个async任务由于某种原因失败,父协程也会失败。这就是为什么async不像launch那样受欢迎的原因之一。 - alexanderktx
    13
    这个答案不正确,它直接将异步函数和挂起函数进行比较,而不是与 launch 进行比较。如果你在示例中直接调用挂起函数,改为使用 launch(Dispatchers.IO) {downloadTask1()},你会发现两者是并发执行的,而不是顺序执行,虽然你无法获得输出结果,但你可以看到它们不是按顺序执行的。此外,如果你不连接 deferred.await(),而是分别调用 deferred.await(),那么 async 就会变成顺序执行。 - Thracian
    17
    这完全是错误的。launchasync都会启动新的协程。你正在将没有子协程的单个协程与有3个子协程的单个协程进行比较。你可以将每个async调用替换为launch,并且并发方面绝对不会有任何变化。 - Salem
    3
    这个例子是错误的。请尝试在启动测试中使用launch{downloadTask1()},而不是downloadTask1() - jakchang
    显示剩余4条评论

    8

    Async和Launch都用于创建在后台运行的协程,两种方式在大多数情况下可以互换使用。

    简短版本:

    如果您不关心任务的返回值,只想执行它,可以使用Launch。 如果您需要任务/协程的返回类型,则应使用Async。

    另一种方法:不过,我认为上述差异/方法是考虑Java /每个请求模型线程的后果。 协程非常便宜,如果您想从某些任务/协程(比如服务调用)的返回值中做些什么,最好从那个任务/协程创建一个新的协程。 如果您想让协程等待另一个协程传输数据,我建议使用通道而不是Deferred对象的返回值。 在我的看法中,使用通道和创建所需数量的协程是更好的方式。

    详细答案:

    唯一的区别在于返回类型和它提供的功能。

    Launch返回 Job ,而Async返回 Deferred 。有趣的是,Deferred扩展了Job。这意味着它必须在Job的基础上提供附加功能。Deferred泛型参数化为其中T是返回类型。因此,Deferred对象可以从由async方法执行的代码块中返回一些响应。

    附言:我撰写了这篇答案,因为我看到了一些在这个问题上事实不正确的答案,并希望为每个人澄清这个概念。此外,在我自己的宠物项目中遇到类似的问题,因为之前有Java背景。


    "Async和Launch都用于创建在后台运行的协程。除非您使用Dispatchers定义,否则协程并不一定意味着在后台执行。您的回答可能会让新手感到困惑。" - Farid

    8

    launch 返回一个工作任务

    async 返回一个结果(延迟的任务)

    launch 结合 join,用于等待任务完成。它会暂停协程,并调用 join(),让当前线程空闲地去做其他事情(例如执行另一个协程)。

    async 用于计算一些结果。它创建一个协程,并将其未来的结果以 Deferred 的形式返回。当返回的延迟任务取消时,正在运行的协程也会被取消。

    考虑一个返回字符串值的异步方法。如果该异步方法不使用 await,它将返回一个 Deferred 字符串,但如果使用 await,则将得到一个字符串作为结果。


    asynclaunch 的关键区别:
    在您的协程执行完后,Deferred 返回特定类型 T 的一个值,而 Job 不会返回值。


    3

    这里输入图片描述

    启动 / 异步无结果

    • 适用于不需要结果的情况,
    • 不会阻塞调用代码的执行,
    • 按顺序运行。

    异步等待结果

    • 当需要等待结果并且可以并行运行以提高效率时使用,
    • 会阻塞调用代码的执行,
    • 并发运行。

    1
    除了其他很好的回答之外,对于熟悉 Rx 并进入协程的人来说,async 返回一个类似于 SingleDeferred,而 launch 返回一个更类似于 CompletableJob。您可以使用 .await() 阻塞并获取第一个值,使用 .join() 阻塞直到完成 Job

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接