Kotlin 协程学习,databinding原理

print(“World!”)

}

// 打印结果:

Thread name:main

Coroutines Thread name:main

Hello, World

可以看到 runBlocking{...}开启了一个协程,但是他没有在新线程里面执行,而是一直阻塞到里面的代码块完成。

可以来看下下面的用法:

fun main() = runBlocking { // 使用runBlocking开启一个协程

launch { // 里面再开启一个协程

delay(1000L)

println("World! " + Thread.currentThread().name)

}

println("Hello, " + Thread.currentThread().name)

}

// 打印结果:

Hello, main

World! main

这个代码中,我们在 runBlocking{}里,又开启了一个launch协程,相当于大协程里面的小协程。

我们没有使用 Thread.sleep()来防止主线程执行完而launch中的代码还没有执行的问题,它依然能够等所有任务都执行完后才关闭程序。

上面两片代码可以得出runBlocking的两个结论

  1. runBlocking{}协程会产生线程阻塞,每当线程执行到这个代码块时,会等到代码块里的所有内容执行完,才会继续走下面的代码

  2. runBlocking{}协程里面可以加入其它的小协程。这个时候,可以把这个代码块看成一个线程的执行顺序(但本身不是线程)

2.3 使用CoroutineScope实现


在讲解 CoroutineScope之前,我们需要先搞清楚: CountineScopecountineScope{...}是两个东西

  • 前者可以创建一个协程作用域

  • 后者产生的是一个挂起函数

他们虽然名称相同,但是作用有很大的不同,我认为官方文档举的例子不是很好,让我饶了很大的弯路。有兴趣的可以读一下这篇:[Coroutines: runBlocking vs coroutineScope

]( )

fun main(){

val coroutineScope = CoroutineScope(Dispatchers.Unconfined) // 1 创建一个CoroutineScope对象

coroutineScope.launch { // 使用该对象开启一个协程

delay(100L) // 模拟一个100ms的延时任务

println(“World”)

}

print("Hello, ")

Thread.sleep(200L) // 让主线程睡200MS以免主线程关闭

}

// 打印结果:

Hello, World

可以看到它和一开始的 GlobalScope.launch{..}的作用差不多,但它们是有许多差别的,在Android并不推荐使用 GlobalScope.launch{..},而是推荐使用 CoroutineScope{..},原因后面会讲到。

注释1中, CoroutineScope的构造函数里面传入的是 Dispatchers.Unconfined(不受限的工作在主线程中),它是一个 CoroutineContext类,它的作用是调度这个协程在哪个线程工作,和Android的Context不一样,叫这个名字是帮助我们去理解。所以这个Context被称为 协程上下文

因为平时经常会遇到线程切换的场景,我们可以利用这个协程上下文来实现线程的切换,在上面的代码中,模拟了一个我们做耗时任务的操作,那么接下来有个场景,我想在后台线程去做这个任务,做完之后把结果拿回主线程进行输出,那我们可能会这么写:

coroutineScope.launch(Dispatchers.IO) {

… // 做耗时任务

launch(Dispatchers.Unconfined) {

… // 去到想要的线程做xxx

}

}

这个时候就发现产生了嵌套!?

确实,如果只是使用launch{...},它本身的能力是有限的, 但是协程中却提供了一个很有帮助的函数:withContext(),这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行,看下下面的一个代码:

fun main() {

val coroutineScope = CoroutineScope(Dispatchers.Unconfined)

coroutineScope.launch(Dispatchers.Unconfined) { // 在未指定线程中开启协程

val text = withContext(Dispatchers.IO) { // 开启一个子协程,并指定在I/O线程,去做一个耗时操作

delay(200L) // 1 做一个200MS的耗时操作

“World”

}

print(text)

}

print("Hello, ")

Thread.sleep(300L) // 让主线程睡300MS,防止主线程关闭

}

// 打印结果:

Hello, World

如果说 withContext(){..}里面的代码很长,或者需要复用,我们需要将他们抽成一个方法,可以写成这样:

coroutineScope.launch(Dispatchers.Unconfined) {

val text = withContext(Dispatchers.IO) {

getText()

}

}

suspend fun getText(): String {

delay(200L)

return “World”

}

我们抽出了一个方法,并在前面加上了 suspend 关键字。

这个关键字是Kotlin协程中最为核心鹅关键字,它的中文意思是 [暂停] 或者 [挂起],它所代表的意义其实就是将当前协程任务在线程中挂起: 线程在执行到遇到挂起的代码时,可以绕过去,不用等到这些代码执行完毕才能执行后面的代码,而是你走你的,我走我的,你占的道反正不在我这。

launch{..}里面的代码块,其实就是默认被 suspend修饰了,所以抽出来的方法,我们也额外的手动加上。

3. 非阻塞式挂起

===========================================================================

3.1 挂起

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

开源分享完整内容戳这里


挂起是什么?请看下面的代码(还是上面那个例子):

coroutineScope.launch(Dispatchers.Unconfined) {

val text = withContext(Dispatchers.IO) {

getText()

}

print(text)

}

上面可以看成做了两个事情:

  • 开启一个(Unconfined即当前线程所在的)协程

  • 在这个协程中 开启一个(IO线程)子协程做耗时任务

这个时候,我们就成 coroutineScope.launch{..}挂起了。

我们以线程的角度来考虑这段代码,请看下图:

在这里插入图片描述

假设在CPU单核运算,只有一条线来走时间片轮换,那我们假设每个时间只有一个线程在执行代码。

那么在走到 withContext(Dispatchers.IO){..}时,它其实就是开启了一个IO线程。并将代码块中的代码放入到这个线程里面去做。

那这不就是切线程吗?为啥美其名曰挂起?

是因为,这个协程的API在完成了I/O线程的任务之后,不需要我们手动切回到原来的线程(可以对比一下RxJava在子线程做完网络请求后我们手动切回主线程),自动的帮我们切回了原来的线程。

我上面的代码,在 withContext(){..} I/O线程做完延时操作后,自然而然的回到了 Unconfined的线程去做print。

这个恢复到原来线程的操作在Kotlin里叫 resume。它本身一点儿也不稀奇,因为我们在RxJava中就已经知道通过Handler来切换线程了, 只是Kotlin包装到我们意识不到原来已经切换回来的程度。

总结成一句话:挂起就是: 切换线程 + 恢复到原来线程

3.2 suspend


我们用一个 suspend 来修饰挂起函数。suspend的意义是什么?它能实现挂起吗?

答案是否定的,单单给一个普通函数加上一个 suspend,没有任何效果,可以看下下面代码:

在这里插入图片描述

我们在函数中调用了 print(),发现 suspend置灰了,这说明只是用 suspend是无法让函数挂起的。

CoroutineScope.launch(){}代码块能让语句挂起吗?答案也是否定的。

那让函数挂起的语句是什么?

答案是 withContext(){..},它是真正的创建了线程的代码,所以当出现了这个语句,协程才能真正的挂起。

当然,除了 withContext() ,还有别的能让协程挂起,比如 delay(),因为他本身也是个操作线程的语句,包括别的。

总结下suspend的作用:

  • 提醒调用者,该函数会切换线程(也就是挂起)

  • 静态代码扫描,检测该函数是否有线程操作

  • 只有在协程中调用withContext(){..}类的代码, 该协程才会被挂起

3.3 非阻塞式挂起的含义


当我们了解了挂起的本质是切换线程,那我们就明白了非阻塞的含义是什么了。

在Java开发中,我们通过调用 Executor来开一个子线程,子线程会阻塞主线程的代码吗?

答案是不会的,因为当我们只是开一个子线程出来,主线程还是会继续走,等时间片不在主线程时,子线程再去走,但对于主线程来说,它里面的代码并没有因为子线程的存在而产生阻塞。

↑所以这个情况,也是非阻塞的。

而Koltin中的【非阻塞式挂起】,其实和Java一样,它只是通过协程达到了这个效果:代码明明看起来是阻塞的,但是运行却是不阻塞的。 如下所示:

coroutineScope.launch(Dispatchers.Main) {

val avatar = async { api.getAvatar(user) } // 获取用户头像

val logo = async { api.getCompanyLogo(user) } // 获取用户所在公司的 logo

val merged = suspendingMerge(avatar, logo) // 合并结果

show(merged) // 更新 UI

}

再往前想一想 runBlocking(){..}为啥线程阻塞了?因为它本身就没有切换线程,它是直接在本线程上做耗时任务。

嘿嘿,是不是有点恍然大悟了?它本身就是噱头。

4. 协程的取消和超时

=============================================================================

官方的例子举得很好,接下来就都用官方的代码了~

4.1 协程的取消


4.1.1 协程的取消

协程任务是可以取消的

// 省略一些CoroutineScope的代码

val job : Job = launch {

repeat(1000) { i ->

println(“job: I’m sleeping $i …”)

delay(500L)

}

}

delay(1300L) // 延迟一段时间

println(“main: I’m tired of waiting!”)

job.cancel() // 取消该作业

job.join() // 等待作业执行结束

println(“main: Now I can quit.”)

// 执行结果:

job: I’m sleeping 0 …

job: I’m sleeping 1 …

job: I’m sleeping 2 …

main: I’m tired of waiting!

main: Now I can quit.

我们可以看到 CoroutineScope 提供了一个 Job类型的句柄,我们可以调用其 Job.cancel()Job.join()来停止和加入该协程。

这两个方法也可以合并成一个 Job.cancelAndJoin()来使用

4.1.2 协程是不能随时取消的

和线程一样,我们在调用了 Thread.cancel()后,线程不会马上取消。

协程也一样,当协程在进行计算任务任务时,如果你直接cancel,是不会取消任务的:

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU

// 每秒打印消息两次

if (System.currentTimeMillis() >= nextPrintTime) {

println(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

}

}

delay(1300L) // 等待一段时间

println(“main: I’m tired of waiting!”)

job.cancelAndJoin() // 取消一个作业并且等待它结束

println(“main: Now I can quit.”)

// 打印结果:

job: I’m sleeping 0 …

job: I’m sleeping 1 …

job: I’m sleeping 2 …

main: I’m tired of waiting!

job: I’m sleeping 3 …

job: I’m sleeping 4 …

main: Now I can quit.

我们需要像 Thread那样,去调用isInterrupted()来检查。

协程提供了一个变量 isActive来判断当前协程是否取消了,如果取消了,就不用再走下去了:

val startTime = System.currentTimeMillis()

val job = launch(Dispatchers.Default) {

var nextPrintTime = startTime

var i = 0

while (isActive) { // 可以被取消的计算循环

// 每秒打印消息两次

if (System.currentTimeMillis() >= nextPrintTime) {

println(“job: I’m sleeping ${i++} …”)

nextPrintTime += 500L

}

}

}

delay(1300L) // 等待一段时间

println(“main: I’m tired of waiting!”)

job.cancelAndJoin() // 取消该作业并等待它结束

println(“main: Now I can quit.”)

// 打印结果:

job: I’m sleeping 0 …

job: I’m sleeping 1 …

job: I’m sleeping 2 …

main: I’m tired of waiting!

main: Now I can quit.

4.1.3 在取消时释放内存

我们是可以感知得到Job被取消的,当Job被取消的时候,它会抛出一个 CancellationException,我们把它当成结束回调,在里面做释放资源:

val job = launch {

try {

repeat(1000) { i ->

println(“job: I’m sleeping $i …”)

delay(500L)

}

} finally {

println(“job: I’m running finally”) // 此时任务取消,释放资源

}

}

delay(1300L) // 延迟一段时间

println(“main: I’m tired of waiting!”)

job.cancelAndJoin() // 取消该作业并且等待它结束

println(“main: Now I can quit.”)

4.1.4 假如你的释放资源也是耗时的怎么办

这是一个比较少见的情况,在 finally块中调用了耗时操作,此时协程又被取消,那相当于你finally的耗时操作执行不了

协程提供了 withContext(NonCancellable){...}来帮助协程延迟取消:

val job = launch {

try {

repeat(1000) { i ->

println(“job: I’m sleeping $i …”)

delay(500L)

}

} finally {

withContext(NonCancellable) {

println(“job: I’m running finally”)

delay(1000L)

println(“job: And I’ve just delayed for 1 sec because I’m non-cancellable”)

}

}

}

delay(1300L) // 延迟一段时间

println(“main: I’m tired of waiting!”)

job.cancelAndJoin() // 取消该作业并等待它结束

println(“main: Now I can quit.”)

4.2 协程的超时


在实践中绝大多数取消一个协程的理由是它有可能超时。

当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout() 函数来做这件事。

来看看示例代码:

withTimeout(1300L) {

repeat(1000) { i ->

println(“I’m sleeping $i …”)

delay(500L)

}

}

// 打印结果:

I’m sleeping 0 …

I’m sleeping 1 …

I’m sleeping 2 …

Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

发现抛出了 TimeoutCancellationException异常,它是 CancellationException的子类。

之所以后者没有产生堆栈信息,是因为协程认为我们取消它,这是一个正常的结束。

如果遇到特殊的场景,比如我必须要知道这个任务在规定时间内还没有做完,,可以使用类似 withTimeoutwithTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

val result = withTimeoutOrNull(1300L) {

repeat(1000) { i ->

println(“I’m sleeping $i …”)

delay(500L)

}

“Done” // 在它运行得到结果之前取消它

}

println(“Result is $result”)

// 打印结果:

I’m sleeping 0 …

I’m sleeping 1 …

I’m sleeping 2 …

Result is null

就不会抛出异常,而是返回一个null。个人认为这样的做法比catch异常要温柔一点。

5. coroutineScope

===================================================================================

coroutineScope{..} 会创建一个挂起函数,所以调用它的函数必须被 suspend修饰。

官方文档是这么描述的:

This function is designed for parallel decomposition of work.

When any child coroutine in this scope fails, this scope fails and all the rest of the children are cancelled

This function returns as soon as the given block and all its children coroutines are completed.

翻译一下就是

这个方法是为了并行工作而设计的

当这个作用域内的任何一个子协程失败,那么这个作用域失败,并且其所有剩余的子协程都失败

当这个作用域所有的子协程都成功完成任务了,那么这个作用域将会返回(结果)

5.1 coroutineScope 和 CoroutineScope的区别


因为 coroutineScope的描述,所以可以从结构化和非结构化入手,coroutineScope是结构化的,CoroutineScope是非结构化的。

结构化更利于并行程序的执行。

举个例子,我们在主线程中,点击一个Button,这个button会去做耗时操作,然后返回结果,使用 CoroutineScope是这样的:

btn.setOnClickListener( () -> {

CoroutineScope(Dispatchers.Main).launch {

int result = downloadUserData()

Toast.makeText(applicationContext, "Result : " + result, Toast.LENGTH_LONG).show()

});

private suspend int downloadUserData() {

int result = 0;

// 这里我们使用一个CoroutineScope并切换线程

CoroutineScope(Dispatchers.IO).launch {

for (int i = 0; i < 2000; i++) {

kotlinx.coroutines.delay(400);

result++;

}

}

return result;

}

}

Toast打印出来的result是0

这是切了线程后,并没有等到该协程执行完返回,直接执行了Toast的展示。

这很明显不是我们想要的结果。

这时,我们换成 coroutineScope {}来试试:

private suspend int downloadUserData() {

int result = 0;

coroutineScope {

for (int i = 0; i < 20000; i++) {

kotlinx.coroutines.delay(400);

result++;

}

}

return result;

}

Toast的打印result是20000,使我们想要的结果。

这就是因为 coroutineScope它等待到代码块执行完,才返回结果。

5.2 coroutineScope 和 runBlocking的区别


猛地一看,coroutineScope{}会等到里面所有的代码执行完,这不跟 runBlocking{}的阻塞等待 一样吗?

他们最大的区别就是,当他们代码块的函数被挂起时,当前的线程会怎么做。

  • coroutineScope{} 函数被挂起时,线程会继续往之后走

  • runBlocking{} 函数被挂起时,线程会等待执行完这个函数里面所有的代码才会往之后走

看个代码:

fun main() {

CoroutineScope(Dispatchers.Unconfined).launch { // 开启一个主协程

val result = downloadUserData()

println(result)

}

println(“OUT”) // 在协程之外打印东西

Thread.sleep(12000L) // 保证程序不结束

}

suspend fun downloadUserData(): Int {

var res = 0

coroutineScope { // 使用coroutineScope来开启一个子协程

for (i in 0…10) {

delay(100L)

res++

}

}

return res

}

// 打印结果:

OUT

11

也就是说,当 coroutineScope挂起时,线程会继续往下走,但由于他没执行完,所以 println(result)没有执行,继续往下走,会执行println("OUT"),所以先打印 OUT , 再打印计算结果。

我们换成runBlocking试试:

suspend fun downloadUserData(): Int {

var res = 0

runBlocking {

for (i in 0…10) {

delay(100L)

res++

}

}

return res

}

// 答应结果:

11

OUT

显然,runBlocking会不会把控制权还给线程,(虽然他挂起来了),但还是会一直执行,直到代码块走完

6. async

==========================================================================

6.1 一个例子


终于来到了关键的地方了,在前面的学习中,我好像只学习了每个关键字、语句块的概念,但是我并没有让他们实践,并行执行,因为学习协程的最终目的是高效进行高并发操作。

我们先来看一下两个耗时操作:

suspend fun doSomethingUsefulOne(): Int {

delay(1000L) // 假设我们在这里做了一些有用的事

return 13

}

suspend fun doSomethingUsefulTwo(): Int {

delay(1000L) // 假设我们在这里也做了一些有用的事

return 29

}

接下来,我们的程序要将两者的最终结果相加,我们可能会这样写:

val time = measureTimeMillis {

val one = doSomethingUsefulOne()

val two = doSomethingUsefulTwo()

println(“The answer is ${one + two}”)

}

println(“Completed in $time ms”)

// 打印结果:

The answer is 42

Completed in 2017 ms

通过耗时来看,我们发现程序它的结果像是串行执行的,没有起到并行的结果。

6.2 使用 async


doSomethingUsefulOne()doSomethingUsefulTwo()并没有产生依赖,并且我们为了更快的得到结果,我们需要将两个方法并行执行。

asynclaunch类似,它可以用来启动一个单独的协程。不同之处在于 launch返回的是Job类型,而 async返回的是一个 Deferred类,这是一个轻量级的非阻塞future,也就是会在稍后提供给我们结果。我们可以使用 .await() 在一个延期的值上得到它的最终结果, Deferred继承自Job,所以我们也可以通过 Deferred来结束一个协程。

val time = measureTimeMillis {

val one = async { doSomethingUsefulOne() }

val two = async { doSomethingUsefulTwo() }

println(“The answer is ${one.await() + two.await()}”) // 使用wait来等待结果

}

println(“Completed in $time ms”)

// 打印结果:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值