Kotlin 协程中,关于 runBlocking, launch ,withContext ,async,doAsync 之间的简单区别

风斗 等级 437 0 0

引入大佬的话,Kotlin的协程,本质上是一个线程框架,它可以方便的切换线程的上下文(如主线程切换到子线程/子线程切回主线程)。而平时我们要想在Android Studio使用协程,先要在gradle引入协程依赖:

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'

首先,创建一个协程的方式有很多种,可以通过 runBlocking,launch (CoroutineScope.lauch / GlobalScope.lauch),withContext ,async 等这些方法来都能创建协程,这些都是我们可能会在协程里用到的常见方法。关于这几个有什么的区别,下面给它大概归为这几类分别进行简单的对比 :

可在全局创建协程的: lauch 与 runBlocking

lauch 与 runBlocking都能在全局开启一个协程,但 lauch 是非阻塞的runBlocking 是阻塞的

下面通过 lauch 来执行一个协程:

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch{
        delay(500)     //延时500ms
        Log.e("TAG","1.执行CoroutineScope.... [当前线程为:${Thread.currentThread().name}]")
    }
    Log.e("TAG","2.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
} 

E: 2.BtnClick.... [当前线程为:main]
E: 1.执行CoroutineScope.... [当前线程为:main]

从上面运行结果可以看出,通过CoroutineScope.launch开启一个协程,协程体里的任务时就会先挂起(suspend),让CoroutineScope.launch后面的代码继续执行,直到协程体内的方法执行完成再自动切回来所在的上下文回调结果。

CoroutineScope.launch 中我们可以看到接收了一个参数Dispatchers.Main,这是一个表示协程上下文的参数,用于指定该协程体里的代码运行在哪个线程。当指定为Dispatchers.Main时,协程体里的代码也是运行在主线程。 当指定为Dispatchers.IO,则当前协程运行在一个子线程里。

还有刚提到了挂起 suspend , 它是Kotlin 中的一个关键字,它一般标识在一个函数的开头,用于表示该函数是个耗时操作,如上面的delay(timeMillis: Long)就是被声明为 suspend 函数。这个关键字主要作用就是为了作一个提醒,并不会因为添加了这个关键字就会该函数立即跑到一个子线程上。suspend 函数是只能在协程体内生效,在Kotlin 协程中,当遇到 suspend 函数的时候 ,该协程会自动逃离当前所在的线程执行任务,此时原来协程所在的线程就继续干自己的事,等到协程的suspend 函数执行完成后又自动切回来原来线程继续往下走。一句话说,suspend就是让协程该走就走,该回来就回来。 但如果协程所在的线程已经运行结束了,协程还没执行完成就不会继续执行了 。为了避免这样的情况就需要结合 runBlocking 来暂时阻塞当前线程,保证代码的执行顺序。

下面再来看通过 runBlocking 来执行一个协程:

 btn.setOnClickListener {
        runBlocking{
            delay(500)    //延时500ms
            Log.e("TAG","1.runBlocking.... [当前线程为:${Thread.currentThread().name}]")
        }
        Log.e("TAG","2.BtnClick.... [当前线程为:${Thread.currentThread().name}]")
    } 

点击按钮后运行结果:
: 1.runBlocking.... [当前线程为:main]
: 2.BtnClick.... [当前线程为:main]

运行结果刚好和上面launch启动的协程例子相反,这里的Log先输出1...再输出2..., 程序会等待执行完成runBlocking内的协程体代码,再执行它后面的代码。 runBlocking里的任务如果是非常耗时的操作时,会一直阻塞当前线程,在实际开发中很少会用到runBlocking。 由于runBlocking 接收的 lambda 代表着一个 CoroutineScope,所以 runBlocking 协程体内可继续通过launch来继续创建一个协程,避免了lauch所在的线程已经运行结束而切不回来的情况。

可返回结果的协程:withContext 与 async

withContext 与 async 都可以返回耗时任务的执行结果。 一般来说,多个 withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行的,async 返回的是一个Deferred,需要调用其await()方法获取结果。

使用 withContext 获取耗时任务结果:定义2个耗时任务,一个2000ms后返回结果,另一个1000ms后返回结果

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = withContext(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }

        val task2 = withContext(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }

        Log.e("TAG", "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis()-time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
} 

运行结果:
: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
: task1 = one , task2 = two , 耗时 3009 ms [当前线程为:main]

从上面结果可以看出,多个withConext是串行执行,如上代码执行顺序为先执行task1再执行task2,共耗时两个任务的所需时间的总和。这是因为withConext是个 suspend 函数,当运行到 withConext 时所在的协程就会挂起,直到withConext执行完成后再执行下面的方法。所以withConext可以用在一个请求结果依赖另一个请求结果的这种情况。

如果同时处理多个耗时任务,且这几个任务都无相互依赖时,可以使用 async ... await() 来处理,将上面的例子改为 async 来实现如下 :

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }

        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }

        Log.e("TAG", "task1 = ${task1.await()}  , task2 = ${task2.await()} , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
} 

运行结果:
: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-3]
: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-3]
: task1 = one , task2 = two , 耗时 2037 ms [当前线程为:main]

改为用async后,运行结果耗时明显比使用withContext更短,且看到与withContext不同的是,task2比task1优先执行完成 。所以说 async 的任务都是并行执行的。但事实上有一种情况例外,我们把await()方法的调用提前到 async 的后面:

btn.setOnClickListener {
    CoroutineScope(Dispatchers.Main).launch {
        val time1 = System.currentTimeMillis()

        val task1 = async(Dispatchers.IO) {
            delay(2000)
            Log.e("TAG", "1.执行task1.... [当前线程为:${Thread.currentThread().name}]")
            "one"  //返回结果赋值给task1
        }.await()

        val task2 = async(Dispatchers.IO) {
            delay(1000)
            Log.e("TAG", "2.执行task2.... [当前线程为:${Thread.currentThread().name}]")
            "two"  //返回结果赋值给task2
        }.await()

        Log.e("TAG", "task1 = $task1  , task2 = $task2 , 耗时 ${System.currentTimeMillis() - time1} ms  [当前线程为:${Thread.currentThread().name}]")
    }
} 

运行结果:
: 1.执行task1.... [当前线程为:DefaultDispatcher-worker-1]
: 2.执行task2.... [当前线程为:DefaultDispatcher-worker-1]
: task1 = one , task2 = two , 耗时 3016 ms [当前线程为:main]

原来await() 仅仅被定义为 suspend 函数,因此直接在async 后面使用 await() 就和 withContext 一样,程序运行到这里就会被挂起直到该函数执行完成才会继续执行下一个 async 。但事实上await()也不一定导致协程会被挂起,await() 只有在 async 未执行完成返回结果时,才会挂起协程。若 async 已经有结果了,await() 则直接获取其结果并赋值给变量,此时不会挂起协程。

anko里的一个扩展函数 doAsync

doAsync 拿出来单独解释,是因为它容易联想到协程中的 async,实际上又与 async 关系不大,因为 doAsync并没有用到协程库中的东西。粗略看了下 doAsync 的源码它的实现都是基于Java的 Future 类进行异步处理和通过Handler进行线程切换 ,从而封装的一个扩展函数方便线程切换。

btn.setOnClickListener {
    doAsync {
        Log.e("TAG", " doAsync...   [当前线程为:${Thread.currentThread().name}]")
        uiThread {
            Log.e("TAG", " uiThread....   [当前线程为:${Thread.currentThread().name}]")
        }
    }
} 

运行结果:
: doAsync... [当前线程为:pool-1-thread-1]
: uiThread.... [当前线程为:main]

可以看到通过doAsync创建了一个线程池,所有耗时方法可以放在 doAsync 上,等获取结果后可以通过 uiThread { } 来切换会主线程。使用 doAsync 进行线程切换让代码看上去像同步的方式实现异步的请求。

收藏
评论区

相关推荐

WorkManager 在 Kotlin 中的实践
WorkManager 是一个 Android Jetpack 扩展库,它可以让您轻松规划那些可延后、异步但又需要可靠运行的任务。对于绝大部分后台执行任务来说,使用 WorkManager 是目前 Android 平台上的最佳实践。 WorkManager https://developer.android.google.cn/topic/libra
vsftpd工作原理和使用详解
vsftpd工作原理和使用详解 简介 FTP(File Transfer Protocol,文件传输协议) 是 TCP/IP 协议组中的协议之一。FTP协议包括两个组成部分,其一为FTP服务器,其二为FTP客户端。其中FTP服务器用来存储文件,用户可以使用FTP客户端通过FTP协议访问位于FTP服务器上的资源。在开发网站的时候,通常利用FTP协议把网页或程
IDEA 配置 Dart 开发环境,以及创建第一个 Dart 项目
IDEA 配置 Dart 开发环境,以及创建第一个 Dart 项目 1、打开 IDEA 会看到如下页面,点击右下角的 Configure,然后点击 Plugins Launch Page 2、在新打开的页面中搜索 Dart,可以看到如下的搜索结果,然后点击右侧面板的 insta
发现Kotlin一个神奇的bug
1、前言 本文将会通过具体的业务场景,由浅入深的引出Kotlin的一个bug,并告知大家这个bug的神奇之处,接着会带领大家去查找bug出现的原因,最后去规避这个bug。 2、bug复现 现实开发中,我们经常会有将Json字符串反序列化为一个对象问题,这里,我们用Gson来写一段反序列代码,如下: kotlin fun <T fromJson(js
GO语言协程的理解
以下内容大部分摘自许世伟的《GO语言核心编程》 最近面试,在自己的简历上写了简单会一些GO语言。结果被面试官问了GO语言goroutine的原理。自己看倒是看过,时间长了又给忘了。特此写下此文以长记性。 协程:协程本质上是一种用户态线程,不需要操作系统来进行抢占式调度,并且在真正的实现中寄存于线程中,因此系统开销极小,可以有效的提高线程任务的并发性,而避
Spring Cloud OAuth2 微服务认证授权
OAuth 2.0 是用于授权的行业标准协议,它致力于简化客户端开发人员的工作,同时为 Web 应用、桌面应用、移动应用等各种客户端应用提供了特定的授权流程。本文讲解如何使用 OAuth2 协议来授权客户端应用访问 Spring Cloud 微服务。 微服务认证授权概述 单点登录 相比于单体应用,微服务应用需要在多个服务之间共享
【python学习】从海贼王中体会进程、线程和协程
在学习Python的过程中,一种最直接的感触就是上手轻松,但精通很难,很多时候离不开计算机学科的基础知识。比如下面这个问题,我是没法第一时间想到准确答案的。 协程和线程有什么区别?在学习的道路上,看懂听懂是输入,写明说清是输出,只有达到输出这一步才算真正搞懂一个知识点。So,今天就先搞搞这个问题,用海贼王的故事来理解个中差异。协程、
5分钟快速梳理你的HTTP体系
HTTP 定义 1. HTTP(超文本传输协议) 是 客户端 与 服务端 之间信息交流的 桥梁。 2. 在信息交流之前必须要做的就是 客户端通过连接TCP/IP协议 80 端口 ,以便 服务端侦听HTTP请求。 3.HTTP 是 一种通用的 , 无状态的应用层协议,基于标准客户机/服务器模型。 HTTP 特点 1.采用 “请求/
Kotlin 协程中,关于 runBlocking, launch ,withContext ,async,doAsync 之间的简单区别
引入大佬的话,Kotlin的协程,本质上是一个线程框架,它可以方便的切换线程的上下文(如主线程切换到子线程/子线程切回主线程)。而平时我们要想在Android Studio使用协程,先要在gradle引入协程依赖: implementation "org.jetbrains.kotlinx:kotlinxcoroutinescore:1.3.3"
聊聊dubbo协议
协议协议通俗易懂地解释就是通信双方需要遵循的约定。我们了解的常见的网络传输协议有tcp、udp、http等。再到我们常用的基础组件,一般来说client端与server端也有相应的协议,如redis、mysql、zookeeper等都是各自约定的私有协议,同样今天标题中的dubbo协议也是一种私有协议,他们都是应用层协议,基于tcp或udp设计。
tcp的三次握手四次挥手
tcp的三次握手流程: 在tcp/ip协议中,tcp协议提供可靠的连接服务,采用三次握手建立一个连接。 第一次握手:建立连接时,客户端发送SYN包【synj】到服务器,并进入SYN_SEND状态,等待服务器确认; 第二次握手:服务器收到SYN包,必须确认客户的SYN(ackj1),同时自己也发送一个SYN包(syn
用 async/await 来处理异步
一级标题昨天看了一篇vue的教程,作者用async/ await来发送异步请求,从服务端获取数据,代码很简洁,同时async/await 已经被标准化,是时候学习一下了。先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 写一个async
理解 Javascript 中的 Async / Await
在本文中,我们将探讨async/await,对于每个Javascript开发人员来说,是异步编程的首选工具。如果您不熟悉javascript,请不要担心,本文将帮助您async/await从头开始理解。 介绍async/await 是javascript中的一种模式,可使您的代码以同步方式执行,但又不影响javascript的异步行为。 定义异步功能要定义一
从 生成器 到 promise+async
本文主要讲解js中关于生成器的相关概念和作用,以及到后面结合 promise 实现 es7中的 async 原理,你将学习到js中异步流程控制相关知识 1、认识生成器思考如下代码:javascript let x 1 function foo() x++ bar() console.log(x) // 3 function bar(
盘点JavaScript中async/await知识
大家好,我是进阶学习者。一、前言Async/await 是以更舒适的方式使用 promise 的一种特殊语法,同时它也非常易于理解和使用。 二、Async function让以 async 这个关键字开始。它可以被放置在一个函数前面。如下所示:async function f() return 1;在函数前面的 “async” 这个单词表达了一个简单的