目录:

  • 一. 协程的基本概念
  • 二. 从异步编程开始
    • 回调
    • CompletableFuture
    • RxJava
    • 协程
  • 三. 协程的基本概念
    • suspend funtion
    • CoroutineScope
    • CoroutineContext
    • CoroutineDispatcher
    • Job 和 Deffered
    • Coroutine builders

一. 协程的基本概念

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。

二. 从异步编程开始

我们先从一个例子说起,发送一个带有认证的 post 请求,需要以下三个步骤,首先客户端向服务其发送一个得到token的请求,然后构造一个 Post 请求,最后将 Post 请求发出去。这三个请求都是耗时操作,而且请求和请求之间有着依赖的关系。

fun requestToken(): Token {delay(500L) // 模拟请求过程return token
}fun createPost(token: Token, item: Item): Post {delay(500L) // 模拟构造过程return post
}fun processPost(post: Post) {delay(500L) // 模拟请求过程
}

方法一:使用回调的方式

操作2依赖于操作1,所以把操作2作为回调放在操作1的参数内,由操作1决定回调时机。

fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }fun postItem(item: Item) {requestTokenAsync { token ->createPostAsync(token, item) { post ->processPost(post)}}
}

这种多层嵌套的方式比较复杂,而且不方便处理异常情况。

方法二:CompletableFuture

Java 8 引入的 CompletableFuture 可以将多个任务串联起来,可以避免多层嵌套的问题。

可以简单看一下API,具体的使用方法参考文章:
CompletableFuture 使用详解

方法 作用
runAsync 创建一个异步操作,不支持返回值
supplyAsync 创建一个异步操作,支持返回值
whenComplete 计算结果完成的回调方法
exceptionally 计算结果出现异常的回调方法
thenApply 当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
handle 与thenApply相似,handle还可以处理异常任务
thenAccept 与thenApply相似,但是没有返回值
thenRun 与thenAccept相似,但是得不到上面任务的处理结果
thenCombine 合并任务,有返回值
thenAcceptBoth 合并任务,无返回值
applyToEither 两个任务用哪个结果
acceptEither 谁返回的结果快使用那个结果
runAfterEither 任何一个完成都会执行下一步操作
runAfterBoth 都完成了才会执行下一步操作
thenCompose 允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。

知道了API后就可以这么写

fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }fun postItem(item: Item) {requestTokenAsync().thenCompose { token -> createPostAsync(token, item) }.thenAccept { post -> processPost(post) }.exceptionally { e ->e.printStackTrace()null}
}

方法三: RxJava

RxJava的用法跟CompletableFuture链式调用比较类似,这也是比较简洁,比较多人使用的方式:

fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }fun postItem(item: Item) {Single.fromCallable { requestToken() }.map { token -> createPost(token, item) }.subscribe({ post -> processPost(post) }, // onSuccess{ e -> e.printStackTrace() } // onError)
}

方法四:协程的方式

suspend fun requestToken(): Token { ... }   // 挂起函数
suspend fun createPost(token: Token, item: Item): Post { ... }  // 挂起函数
fun processPost(post: Post) { ... }fun postItem(item: Item) {GlobalScope.launch {val token = requestToken()val post = createPost(token, item)processPost(post)// 需要异常处理,直接加上 try/catch 语句即可}
}

协程可以让我们使用顺序的方式去写异步代码,而且并不会阻塞UI线程。

三. 协程的基本概念

1. suspend funtion

我们写的有两个方法是挂起的函数(suspend function)

suspend fun requestToken(): Token { ... }
suspend fun createPost(token: Token, item: Item): Post { ... }

首先要知道的是,挂起函数挂起协程的时候,并不会阻塞线程。

然后一个 suspend function 只能在一个协程或一个 suspend function 中使用,但是suspend function和普通函数使用方法一样,有自己的参数,有自己的返回值,那么为什么要使用suspend funtion呢?

我们可以看到delay函数是一个挂起函数 , Thread.sleep()是一个阻塞函数,如果我们在一个A函数可能会挂起协程,比如调用delay()方法,因为 delay() 是suspend function ,只能在一个协程或一个suspend function中使用,所以A函数也必须是suspend function。所以使用suspend funtion的标准是该函数有无挂起操作。

public suspend fun delay(timeMillis: Long) {if (timeMillis <= 0) return // don't delayreturn suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)}
}

2. CoroutineScope

CoroutineScope为协程的作用域,可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。

创建子scope的方式有许多种, 常见的方式有:

方法一:使用lauch, async 等builder创建一个新的子协程。

我们来看一下CoroutineScop接口

// 每个Coroutine作用域都有一个Coroutine上下文
public interface CoroutineScope {// Scope 的 Contextpublic val coroutineContext: CoroutineContext
}

所以 CoroutineScope 只是定义了一个新 Coroutine 的 coroutineContext,其实每个 coroutine builder(launch
,async) 都是 CoroutineScope 的扩展函数,并且自动的继承了当前 Scope 的 coroutineContext 和取消操作。我们以launch方法为例:

public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit
): Job {val newContext = newCoroutineContext(context)val coroutine = if (start.isLazy)LazyStandaloneCoroutine(newContext, block) elseStandaloneCoroutine(newContext, active = true)coroutine.start(start, coroutine, block)return coroutine
}
  • 第一个参数 context,默认 launch 所创建的 Coroutine 会自动继承当前 Coroutine 的 context,如果有额外的 conetxt 需要传递给所创建的 Coroutine 则可以通过第一个参数来设置。

  • 第二个参数 start 为 CoroutineStart 枚举类型,用来指定 Coroutine 启动的选项。有如下几个取值:
    - DEFAULT (默认值)立刻安排执行该Coroutine实例
    - LAZY 延迟执行,只有当用到的时候才执行
    - ATOMIC 类似 DEFAULT,区别是当Coroutine还没有开始执行的时候无法取消
    - UNDISPATCHED 如果使用 Dispatchers.Unconfined dispatcher,则立刻在当前线程执行直到遇到第一个suspension point。然后当 Coroutine 恢复的时候,在继续在 suspension的 context 中设置的 CoroutineDispatcher 中执行。

  • 第三个参数 block 为一个 suspending function,这个就是 Coroutine 中要执行的代码块,在实际使用过程中通常使用 lambda 表达式,也称之为 Coroutine 代码块。需要注意的是,这个 block 函数定义为 CoroutineScope 的扩展函数,所以在代码块中可以直接访问 CoroutineScope 对象(也就是 this 对象)

结论:launch方法实际上就是new了一个LazyStandaloneCoroutine协程(isLazy属性为false),协程自动的继承了当前 Scope(this代表的协程scope) 的 coroutineContext 和取消操作。

方法二:使用coroutineScope Api创建新scope:

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R

这个api主要用于方便地创建一个子域(相当于创建一个局部作用域),并且管理域中的所有子协程。注意这个方法只有在所有 block中创建的子协程全部执行完毕后,才会退出。

// print输出的结果顺序将会是 1, 2, 3, 4
coroutineScope {delay(1000)println("1")launch { delay(6000) println("3")}println("2")return@coroutineScope}println("4")

方法三:继承CoroutineScope.这也是比较推荐的做法,用于处理具有生命周期的对象。

在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的 Coroutine 只在该界面有意义,如果用户在等待 Coroutine 执行的时候退出了这个界面,则再继续执行这个 Coroutine 可能是没必要的。那么我们怎么让activity管理好其内的 Coroutine 呢?

我们来看下面的例子:

class ScopedActivity : Activity(), CoroutineScope {lateinit var job: Job// CoroutineScope 的实现override val coroutineContext: CoroutineContextget() = Dispatchers.Main + joboverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)job = Job()}override fun onDestroy() {super.onDestroy()// 当 Activity 销毁的时候取消该 Scope 管理的 job。// 这样在该 Scope 内创建的子 Coroutine 都会被自动的取消。job.cancel()}/** 注意 coroutine builder 的 scope, 如果 activity 被销毁了或者该函数内创建的 Coroutine* 抛出异常了,则所有子 Coroutines 都会被自动取消。不需要手工去取消。*/fun loadDataFromUI() = launch { // <- 自动继承当前 activity 的 scope context,所以在 UI 线程执行val ioData = async(Dispatchers.IO) { // <- launch scope 的扩展函数,指定了 IO dispatcher,所以在 IO 线程运行// 在这里执行阻塞的 I/O 耗时操作}// 和上面的并非 I/O 同时执行的其他操作val data = ioData.await() // 等待阻塞 I/O 操作的返回结果draw(data) // 在 UI 线程显示执行的结果}
}

解释一下这个地方:get() = Dispatchers.Main + job

一个上下文(context)可以是多个上下文的组合。组合的上下文需要是不同的类型。所以,你需要做两件事情:

  • 一个 dispatcher: 用于指定协程默认使用的 dispatcher;
  • 一个 job: 用于在任何需要的时候取消协程;

操作符号 + 用于组合上下文。如果两种不同类型的上下文相组合,会生成一个组合的上下文(CombinedContext),这个新的上下文会同时拥有被组合上下文的特性。因为:get() = Dispatchers.Main + job,所以launch方法实际上是在Dispatchers.Main,也就是在UI线程中执行的。

3. CoroutineContext

CoroutineScope 可以理解为一个协程,里面有一个协程的上下文:CoroutineContext,这个协程上下文包含很多该协程的信息,比如:Job, ContinuationInterceptor, CoroutineName 和CoroutineId。在CoroutineContext中,是用map来存这些信息的, map的键是这些类的伴生对象,值是这些类的一个实例,你可以这样子取得context的信息:

val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]

4. CoroutineDispatcher

CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有四种标准实现Dispatchers.Default、Dispatchers. IO,Dispatchers.Main 和 Dispatchers.Unconfined,Unconfined 就是不指定线程。

  • Dispatchers.Default: 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。
  • Dispatchers. IO: 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism 可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。
  • Dispatchers.Unconfined: 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point。也就是说,coroutine builder 函数在遇到第一个 suspension point 的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function 所在的线程。 一般而言我们不使用 Unconfined。
  • Dispatchers.Main: 是在 Android 的 UI 线程执行。
  • 通过 newSingleThreadContext 和 newFixedThreadPoolContext 函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。

5. Job 和 Deffered

CoroutineScope.launch 函数返回一个 Job 对象,该对象代表了这个刚刚创建的 Coroutine实例,job 对象有不同的状态(刚创建的状态、活跃的状态、执行完毕的状态、取消状态等),通过这个 job 对象可以控制这个 Coroutine 实例,比如调用 cancel 函数可以取消执行。Job对象持有所有的子job实例,可以取消所有子job的运行。Job的join方法会等待自己以及所有子job的执行, 所以Job给予了CoroutineScope一个管理自己所有子协程的能力。

CoroutineScope.async 函数也是三个参数,参数类型和 launch 一样,唯一的区别是第三个block参数会返回一个值,而 async 函数的返回值为 Deferred 类型。可以通过 Deferred 对象获取异步代码块(block)返回的值。Deferred 继承了 Job,它有个 await() 方法。

// Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete,
// returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
public suspend fun await(): T

6. Coroutine builders

  1. CoroutineScope.launch : 不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,无返回值
  2. CoroutineScope.async : 在后台创建一个新协程,有返回值
  3. runBlocking :

创建一个新的协程来阻塞当前线程,直到 runBlocking 代码块执行完成。通常它不会用于协程中,因为在协程中写一个阻塞的代码块实在太别扭,可以通过挂起操作取代。它通常作为一个适配器,将 main 线程转换成一个 main 协程,我们也就持有了一个 main 协程的 coroutineContext 上下文对象,就可以随心所欲用(this)使用 coroutineContext 的扩展方法,随心所欲使用 suspend 方法 ( suspend 方法只能用于 suspend 方法和协程中)。所以 runBlocking 一般用在 test 函数和 main 函数中。

  1. withContext :

withContext 不会创建一个新的协程,在指定的协程上运行代码块,并挂起该协程直到代码块运行完成。通常是用于切换协程的上下文。

例如:

// 使用 withContext 切换协程,上面的例子就是先在 IO 线程里执行,然后切换到主线程。
GlobalScope.launch(Dispatchers.IO) {...withContext(Dispatchers.Main) {...}
}

Kotlin协程简介(一)相关推荐

  1. Kotlin协程简介(一) Hello,coroutines!

    协程的作用 协程并不是一个新鲜概念,相信大家都有所了解,它的好处是可以极大程度的复用线程,通过让线程满载运行,达到最大程度的利用CPU,进而提升应用性能.它和反应式编程一样都可以有效的提高资源的利用率 ...

  2. Kotlin协程简介

    1.  什么是协程 关于协程的定义有很多,在Kotlin语言中,协程比较合理的定义应该是一个线程框架(扔物线)或者说是一种并发设计模式(官方).它是由官方设计的一套API方便开发者进行多线程开发. 2 ...

  3. 探索 Kotlin 协程原理

    接下来跟大家分享一下我在了解 Kotlin 协程实现的过程中理解的一些概念,如果你发现哪些地方我说错了的话,欢迎提出你的理解. 1. Kotlin 协程原理概述 Kotlin 协程的大致的执行流程如上 ...

  4. Kotlin协程 - - - 协程的简单使用

    一.协程介绍 1.简介 协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码.协程是在版本 1.3 中添加到 Kotlin 的,它基于来自其他语言的既定概念. 在 And ...

  5. 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

    前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...

  6. Kotlin 协程:简单理解 runBlocking, launch ,withContext ,async,doAsync

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

  7. 在 Android 开发中使用 Kotlin 协程 (一) -- 初识 Kotlin 协程

    前言 最近在研究 Kotlin 协程,发现功能真的超级强大,很有用,而且很好学,如果你正在或计划使用 Kotlin 开发 Android,那么 Kotlin 协程你一定不能错过! 协程是什么? 我们平 ...

  8. 分析Kotlin协程只挂起不恢复会怎样(是否存在协程泄漏),以及挂起的协程存在哪里?

    前言 刚开始正式学协程原理的时候(以前只是学api怎么用),大概是20年6月,也就是bennyhuo大佬出书<深入理解Kotlin协程>的时候,我买了本然后细细研究,我的内心就一直有一个问 ...

  9. 动手实现Kotlin协程同步切换线程,以及Kotlin协程是如何实现线程切换的

    前言 突发奇想想搞一个同步切换线程的Kotlin协程,而不用各种withContext(){},可以减少嵌套且逻辑更清晰,想实现的结果如下图: 分析 实现我们想要的结果,首先需要知道协程为什么可以控制 ...

最新文章

  1. 打造新型智慧城市标杆 金华跻身中国城市信息化50强
  2. Socket 实现聊天功能
  3. 菜鸟学习笔记:Java基础篇2(变量、运算符、流程控制语句、方法)
  4. 如何把Java的double类型变量保留两位小数
  5. 【Redis】redis数据类型及应用场景
  6. html 忽略标签属性,限制文本一定数目的字符忽略HTML标签/属性
  7. MATLAB滤波demo
  8. 项目管理:系统需求分析模板
  9. win7下安装vc6.0(sp5,sp6补丁安装)
  10. CF1132D Stressful Training
  11. [教程] ESP32+TFT+分光棱镜实现透明小电视
  12. JAVA用Math 给pi赋值_导入Math.PI作为参考或值
  13. 多种方式带你玩转 javascript 实现关闭浏览器页签
  14. 古诗词 中文 分词 自动化
  15. 美狐美颜SDK动态贴纸代码浅析
  16. 2019 原创技术干货整理
  17. galaxy nexus升级包takju-jop40d的boot.img和recovery.img中ramdisk对比
  18. python中如何保存并使用训练好的模型并调用
  19. Ajax——判断用户名是否已经注册
  20. 微信小程序 pages使用

热门文章

  1. Win7 od下send断点
  2. cocos2d-x初探学习笔记(15)--CCOrbitCamera
  3. Linux网络编程 | Socket编程(二)TCPSocket的封装、TCP服务器多进程、多线程版本的实现
  4. 数据结构与算法 | 带头双向循环链表
  5. 第27讲:令人抓狂的 JavaScript 混淆技术
  6. 一篇漫画,看懂云计算!
  7. 你了解HTTPS工作原理吗?
  8. 一个 bad file descriptor 的问题
  9. 【从上云到创新,视频云的新技术与新场景】
  10. 通过Canvas在浏览器中更酷的展示视频