一、什么是协程
说明:仅限于 JVM和Android上,协程就是一个类似安卓handler和java中线程池的一种线程框架,协程只是对线程高级封装的API,协程的本质还是线程——协程=漂亮的多线程。
1、协程挂起:
就是一个能够自动切换回来的线程。执行一个挂起函数,在执行完挂起函数任务后会切换回该协程调度器指定的线程中去(也可能是切回原来的线程中去),挂起函数和协程都可以通过调度器指定运行在不同的线程。一个挂起函数只能在另个挂起函数或者协程中被调用。挂起函数必须要带suspend修饰,但不是说被suspend修饰的函数就是线程切换的点,而是这个挂起函数有直接或间接的调用kotlin协程内自带的挂起函数来实现挂起,从而才实现线程的切换;suspend修饰只是提醒kotlin这一块是一个耗时操作,避免卡顿。
注意:
1.单协程内多 suspend 函数运行:suspend 函数挂起,挂起的是当前suspend 函数所在的协程;一个协程内的多个suspend 挂起函数是顺序执行的,上面的suspend函数没执行完是不会执行其他的suspend函数的,suspend 函数会将整个协程挂起,而不仅仅是这个 suspend 函数。
2.这里有别于一个协程内创建多个子协程去挂起,协程内的单个子协程挂起了会去执行其他子协程,务必不要搞混了。withContext挂起的是内部包裹的代码块,阻塞当前运行withContext的协程。一个withContext和一个delay都是可以实现挂起,withContext挂起时间取决于包裹的代码块运行时间,delay是直接设置挂起时间,delay挂起阻塞当前运行delay的协程,delay挂起后不影响爷爷层的协程运行其他子协程。
runBlocking {
val job: Job = GlobalScope.launch(Dispatchers.IO) {
println("AA----协程 开始执行,时间: ${System.currentTimeMillis()}")
val job1 = withContext(Dispatchers.IO) {
println("AA----getToken 开始执行,时间: ${System.currentTimeMillis()}")
delay(1000)
println("AA----getToken 开始执行,挂起后时间: ${System.currentTimeMillis()}")
}
println("CC----setText 执行,--时间==0: ${System.currentTimeMillis()}")
val job2 = withContext(Dispatchers.IO) {
println("BB----getResponse 开始执行,时间: ${System.currentTimeMillis()}")
delay(1200)
println("BB----getResponse 开始执行,挂起后时间: ${System.currentTimeMillis()}")
}
println("DD----setText 执行,--时间==1: ${System.currentTimeMillis()}")
}
job.join()
}

runBlocking {
val job: Job = GlobalScope.launch(Dispatchers.IO) {
val job1 = GlobalScope.launch(Dispatchers.IO) {
println("-A---" + System.currentTimeMillis())
delay(1200L)
println("-A-" + System.currentTimeMillis() + Thread.currentThread().name)
}
println("-----AA----" + "主携程--0==" + System.currentTimeMillis())
val job2 = GlobalScope.launch(Dispatchers.IO) { // 创建一个协程作用域
launch {
println("-B----" + System.currentTimeMillis())
delay(1500L)
println("-B-" + System.currentTimeMillis())
}
println("-C-----" + System.currentTimeMillis()) // 这一行会在内嵌 launch 之前输出
delay(500L)
println("-C-" + System.currentTimeMillis() + Thread.currentThread().name) // 这一行会在内嵌 launch 之前输出
}
println("-----AA----" + "主携程--1==" + System.currentTimeMillis())
val job3 = GlobalScope.launch(Dispatchers.IO) { // 创建一个协程作用域
launch {
println("-D----" + System.currentTimeMillis())
delay(1500L)
println("-D-" + System.currentTimeMillis())
}
println("-E-----" + System.currentTimeMillis()) // 这一行会在内嵌 launch 之前输出
delay(300L)
println("-E-" + System.currentTimeMillis() + Thread.currentThread().name) // 这一行会在内嵌 launch 之前输出
}
println("-----AA----" + "主携程--3==" + System.currentTimeMillis())
job1.join()
job2.join()
job3.join()
println("Coroutine scope is over" + System.currentTimeMillis() + Thread.currentThread().name) // 这一行在内嵌 launch 执行完毕后才输出
}
job.join()
}

3.一个线程内创建的多个子协程,一个协程挂起,当前线程就会去执行其他协程。
4.runBlocking {}会等待所有子协程执行完毕
2、非阻塞式挂起:
就是用阻塞式的代码写法,实现了非阻塞式的功能(同步代码写法实现异步任务)
runBlocking本身会阻塞当前线程去等待。协程的async异步 和协程的 launch是相似的,只是它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch
返回一个 Job 并且不附带任何结果值,而 async
返回一个 Deferred 包裹的值—— >一个轻量级的非阻塞协程。async协程默认是等待状态的,创建后不会立即执行会有延迟,但是如果调用.await()方法会立即执行并且是阻塞父协程的;如果设置了启动模式async(start = CoroutineStart.LAZY),则只有调用了.await()方法才会启动协程返回一个值。调用.start()也可以启动,但是这样只是单单异步启动而无返回值。
3、await()和join()
都会阻塞当前父协程去等待子协程执行完毕,只是await()可以返回一个结果值



4、UNDISPATCHED:
这个模式是立即执行,其他模式是立即调度或者等待调度,立即执行比立即调度优先级高,发起调度接下来才会有执行操作。





5、大写的CoroutineScope
没有继承runBlocking协程的上下文,所以这种情况的runBlocking不会等待子协程CoroutineScope执行完毕。大写的CoroutineScope与小写的coroutineScope的区别:小写的coroutineScope叫做协程构建器,里面自带继承父协程的上下文作用域,而大写的作用域是自定义设置的协程上下文作用域。

6、Flow流:
是冷流,就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,用于替代RxJava。从源码可知folw内是执行了挂起函数的,flow是协程中的库,只能用于协程环境。
冷流:从0到1的过程,只有调用了collect末端操作符才会去运行流,才会调用emit()发送动作;——开始消费时才生产数据,不消费则不生产
热流:从0.5到1的过程,有之前的基础,不管你用不用,创建后就已经默默去实现了
Flow切换线程的方式与协程切换线程是类似的都是通过启动一个子协程,然后通过CoroutineContext中的Dispatchers切换线程,不同的地方在于Flow切换过程中利用了Channel通道来传递数据。
Flow有以下特点:
1.冷数据流,流是生产者,末端操作符是消费者,不消费则不生产数据(不调用末端操作符就不调用emit发射),这一点与Channel正相反:Channel的发送端并不依赖于接收端。
2.Flow通过flowOn改变数据发射的线程,数据消费线程则由协程所在线程决定
3.与RxJava类似,支持通过catch捕获异常,通过onCompletion 回调完成
4.Flow没有提供取消方法,可以通过取消Flow所在协程的方式来取消
具体使用:
lifecycleScope.launch {
flow {
//生产者
for (i in1..10)
{ emit(i) }
}.flowOn(Dispatchers.Main)//上游流所运行线程
.catch
{
//异常处理
} .onCompletion {
//完成回调
} .collect {
num ->
// 消费者,具体的消费处理
// 只有collect时才会调用发送流的生产数据
//下游接收不可以设置运行线程,保持与父协程相同运行线程
}
}
7、Channel通道:
从本质上来看,计算机上线程和协程同步信息其实都是通过共享内存来进行的,因为无论是哪种通信模型,线程或者协程最终都会从内存中获取数据,Channel的底层实现也需要对共享内存加锁来实现。
既然都是共享内存那和我们自己使用共享内存有什么区别呢?所以更为准确的说法是为什么我们使用发送消息的方式来同步信息,而不是多个线程或者协程直接共享内存?
1.首先,使用发送消息来同步信息相比于直接使用共享内存和互斥锁是一种更高级的抽象,使用更高级的抽象能够为我们在程序设计上提供更好的封装,让程序的逻辑更加清晰;
2.其次,消息发送在解耦方面与共享内存相比也有一定优势,我们可以将线程的职责分成生产者和消费者,并通过消息传递的方式将它们解耦,不需要再依赖共享内存;
3.最后,选择使用消息发送的方式,通过保证同一时间只有一个活跃的线程能够访问数据,能够从设计上天然地避免线程竞争和数据冲突的问题;

网友评论