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

What is the difference between launch/join and async/await in Kotlin coroutines

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

  • launch is used to fire and forget coroutine. It is like starting a new thread. If the code inside the launch terminates with exception, then it is treated like uncaught exception in a thread -- usually printed to stderr in backend JVM applications and crashes Android applications. join 用于等待已启动协程的完成,它不会传播其异常。但是,崩溃的 child 协程也会取消其父协程并出现相应的异常。

  • async is used to start a coroutine that computes some result. The result is represented by an instance of Deferred and you must use await就可以了。 async 代码中未捕获的异常存储在生成的 Deferred 中并且不会传递到其他任何地方,除非被处理,否则它将被静默丢弃。 你绝不能忘记你用 async 启动的协程。

我发现 this guide 很有用。我会引用重要的部分。

协程

Essentially, coroutines are light-weight threads.

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

启动

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 启动一个协程,做一些事情,然后 returns 立即作为 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
}

异步

Conceptually, async is just like launch. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch returns a Job and does not carry any resulting value, while async returns a Deferred -- a light-weight non-blocking future that represents a promise to provide a result later.

所以 async 启动一个后台线程,做一些事情,然后 returns 一个令牌立即作为 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")
}

You can use .await() on a deferred value to get its eventual result, but Deferred is also a Job, so you can cancel it if needed.

所以Deferred实际上是一个JobRead this了解更多详情。

interface Deferred<out T> : Job (source)

async 默认是 eager

There is a laziness option to async using an optional start parameter with a value of CoroutineStart.LAZY. It starts coroutine only when its result is needed by some await or if a start function is invoked.

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 进度条,直到所有下载完成。有 3 个 suspend 函数 downloadTask1()downloadTask2()downloadTask3() 下载数据。为了模拟它,我在这些函数中使用了 delay()。这些函数分别等待 5 seconds8 seconds5 seconds

因为我们已经使用 launch 来启动这些挂起函数,所以 launch按顺序(一个接一个)执行它们。这意味着,downloadTask2() 将在 downloadTask1() 完成后开始,而 downloadTask3() 将仅在 downloadTask2() 完成后开始。

如输出屏幕截图 Toast 所示,完成所有 3 次下载的总执行时间将导致 5 秒 + 8 秒 + 5 秒 = 18 秒 launch

  1. async

正如我们所见,launch 对所有 3 个任务执行了 sequentially。完成所有任务的时间是 18 seconds

如果这些任务是独立的,并且不需要其他任务的计算结果,我们可以使它们运行 concurrently。它们将同时启动并 运行 在后台同时启动。这可以通过 async.

来完成

async returns 是 Deffered<T> 类型的实例,其中 T 是我们暂停函数 returns 的数据类型。例如,

  • downloadTask1() 会 return Deferred<String> 因为字符串是 return 类型的函数
  • downloadTask2() 会 return Deferred<Int> 因为 Int 是 return 类型的函数
  • downloadTask3() 会 return Deferred<Float> 因为 Float 是 return 类型的函数

我们可以使用 Deferred<T> 类型 async 中的 return 对象来获取 T 类型的 returned 值。这可以通过 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 seconds,这是 downloadTask2() 的时间,因为它是所有 3 个任务中最长的。您可以在 Toast message

中的以下屏幕截图中看到这一点

启动return工作

异步 return结果(延迟作业)

launchjoin 用于等待作业完成。它只是暂停协程调用 join(),同时让当前线程可以自由地做其他工作(比如执行另一个协程)。

async用于计算一些结果。它创建了一个协程,return 将其未来的结果作为 Deferred 的实现。 运行 协程在取消生成的延迟时被取消。

考虑一个 return 是字符串值的异步方法。如果在没有 await 的情况下使用异步方法,它将 return 一个 Deferred 字符串,但是如果使用 await 你将得到一个字符串作为结果


asynclaunch的主要区别:
在协程完成执行后延迟 return 类型 T 的特定值,而 Job 不会。

  1. 这两个协程构建器,即 launch 和 async 基本上都是具有 CoroutineScope 类型接收器的 lambda,这意味着它们的内部块被编译为挂起函数,因此它们都 运行 在异步模式下并且他们都将按顺序执行他们的块。

  2. launch 和 async 的区别在于它们启用了两种不同的可能性。启动构建器 return 是一个作业,但是异步函数 return 是一个延迟对象。您可以使用 launch 来执行您不希望从中获得任何 returned 值的块,即写入数据库或保存文件或处理一些基本上只是因为其副作用而调用的东西。另一方面,async which return a Deferred 正如我之前所说 returns 是执行其块的有用值,它是一个包装数据的对象,因此您可以将它主要用于其结果但可能也是因为它的副作用。 N.B:您可以剥离 deferred 并使用函数 await 获取其值,这将阻止语句的执行,直到值被 returned 或抛出异常!您可以通过使用函数 join()

  3. 来实现与启动相同的目的
  4. 协程构建器(启动和异步)均可取消。

  5. 还有什么吗?:是的,如果在其块内抛出异常,则启动时协程会自动取消并传递异常。另一方面,如果异步发生这种情况,则异常不会进一步传播,并且应该在 returned Deferred 对象中 caught/handled。

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

启动/异步无结果

  • 不需要结果时使用,
  • 不要屏蔽调用的代码,
  • 运行 连续

结果异步

  • 需要等待结果时可以运行并行 效率,
  • 屏蔽调用的代码,
  • 运行并发

除了其他很好的答案,对于熟悉 Rx 和进入协程的人来说,async returns 一个 Deferred 类似于 Singlelaunch returns 更类似于 CompletableJob。您可以 .await() 阻塞并获取第一个的值,并且 .join() 阻塞直到 Job 完成。

Async 和 Launch,两者都用于创建在后台 运行 的协程。几乎在任何情况下都可以使用它们中的任何一种。

tl;dr 版本:

当您不关心任务的 return 价值,只想 运行 它时,您可以使用 Launch。如果您需要 task/coroutine 中的 return 类型,您应该使用异步。

备用: 但是,我觉得上面的 difference/approach 是根据每个请求模型 Java/one 线程思考的结果。协程非常便宜,如果您想根据某些 task/coroutine 的 return 值执行某些操作(比如服务调用),您最好从那个协程创建一个新的协程。如果你想让协程等待另一个协程传输一些数据,我建议使用通道而不是 Deferred 对象的 return 值。使用通道并根据需要创建尽可能多的协程,是 IMO

更好的方法

详细解答:

唯一的区别在于 return 类型及其提供的功能。

启动 returns Job,同时异步 returns Deferred。有趣的是,Deferred 扩展了 Job。这意味着它必须在 Job 之上提供额外的功能。 Deferred 是类型参数化的,其中 T 是 return 类型。因此,延迟对象可以 return 来自异步方法执行的代码块的一些响应。

p.s。之所以写这个答案,是因为看到这个问题有一些不符合事实的答案,想给大家理清概念。此外,在我自己从事宠物项目时,由于以前的 Java 背景,我遇到了类似的问题。