电子说
通过launch
在一个协程中启动子协程,可以根据业务需求创建一个或多个子协程:
fun launchTest3() {
print("start")
GlobalScope.launch {
delay(1000)
print("CoroutineScope.launch")
//在协程内创建子协程
launch {
delay(1500)//1.5秒无阻塞延迟(默认单位为毫秒)
print("launch 子协程")
}
}
print("end")
}
打印数据如下:
launch3.gif
async
类似于launch
,都是创建一个不会阻塞当前线程的新的协程。它们区别在于:async
的返回是Deferred
对象,可通过Deffer.await()
等待协程执行完成并获取结果,而 launch
不行。常用于并发执行-同步等待和获取返回值的情况。
public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred
launch
。launch
。launch
。Job
,一个有结果的Job
,可通过Deffer.await()
等待协程执行完成并获取结果。//获取返回值
fun asyncTest1() {
print("start")
GlobalScope.launch {
val deferred: Deferred<String> = async {
//协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
delay(2000)//2秒无阻塞延迟(默认单位为毫秒)
print("asyncOne")
"HelloWord"//这里返回值为HelloWord
}
//等待async执行完成获取返回值,此处并不会阻塞线程,而是挂起,将线程的执行权交出去
//等到async的协程体执行完毕后,会恢复协程继续往下执行
val result = deferred.await()
print("result == $result")
}
print("end")
}
上面例子中返回对象Deferred
, 通过函数await()
获取结果值。打印数据如下:
async1.gif
注意:await()
不能在协程之外调用,因为它需要挂起直到计算完成,而且只有协程可以以非阻塞的方式挂起。所以把它放到协程中。
当在协程作用域中使用async
函数时可以创建并发任务:
fun asyncTest2() {
print("start")
GlobalScope.launch {
val time = measureTimeMillis {//计算执行时间
val deferredOne: Deferred<Int> = async {
delay(2000)
print("asyncOne")
100//这里返回值为100
}
val deferredTwo: Deferred<Int> = async {
delay(3000)
print("asyncTwo")
200//这里返回值为200
}
val deferredThr: Deferred<Int> = async {
delay(4000)
print("asyncThr")
300//这里返回值为300
}
//等待所有需要结果的协程完成获取执行结果
val result = deferredOne.await() + deferredTwo.await() + deferredThr.await()
print("result == $result")
}
print("耗时 $time ms")
}
print("end")
}
打印数据如下:
async2.gif
上面的代码就是一个简单的并发示例,async
是不阻塞线程的,也就是说上面三个async{}
异步任务是同时进行的。通过await()
方法可以拿到async
协程的执行结果,可以看到两个协程的总耗时是远少于9秒的,总耗时基本等于耗时最长的协程。
1.
Deferred
集合还可以使用awaitAll()
等待全部完成;2.如果
Deferred
不执行await()
则async
内部抛出的异常不会被logCat
或tryCatch
捕获, 但是依然会导致作用域取消和异常崩溃; 但当执行await时异常信息会重新抛出。3.惰性并发,如果将
async
函数中的启动模式设置为CoroutineStart.LAZY
懒加载模式时则只有调用Deferred
对象的await
时(或者执行async.satrt()
)才会开始执行异步任务。
launch
构建器适合执行 "一劳永逸" 的工作,意思就是说它可以启动新协程而不需要结果返回;async
构建器可启动新协程并允许您使用一个名为await
的挂起函数返回result
,并且支持并发。另外launch
和async
之间的很大差异是它们对异常的处理方式不同。如果使用async
作为最外层协程的开启方式,它期望最终是通过调用 await
来获取结果 (或者异常),所以默认情况下它不会抛出异常。这意味着如果使用 async
启动新的最外层协程,而不使用await
,它会静默地将异常丢弃。
反观线程,java平台上很明确地给出了线程的类型Thread
,我们也需要一个这样的类来描述协程,它就是Job
。它的API设计与Java的Thread
殊途同归。
Job
是协程的句柄。如果把门和门把手比作协程和Job
之间的关系,那么协程就是这扇门,Job
就是门把手。意思就是可以通过Job
实现对协程的控制和管理。
从上面可以知道Job
是launch
构建协程返回的一个协程任务,完成时是没有返回值的。可以把Job
看成协程对象本身,封装了协程中需要执行的代码逻辑,协程的操作方法都在Job
身上。Job
具有生命周期并且可以取消,它也是上下文元素,继承自CoroutineContext
。
这里列举Job
几个比较有用的函数:
public interface Job : CoroutineContext.Element {
//活跃的,是否仍在执行
public val isActive: Boolean
//启动协程,如果启动了协程,则为true;如果协程已经启动或完成,则为false
public fun start(): Boolean
//取消Job,可通过传入Exception说明具体原因
public fun cancel(cause: CancellationException? = null)
//挂起协程直到此Job完成
public suspend fun join()
//取消任务并等待任务完成,结合了[cancel]和[join]的调用
public suspend fun Job.cancelAndJoin()
//给Job设置一个完成通知,当Job执行完成的时候会同步执行这个函数
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
}
与Thread
相比,Job
同样有join()
,调用时会挂起(线程的join()
则会阻塞线程),直到协程完成;它的cancel()
可以类比Thread
的interrupt()
,用于取消协程;isActive
则是可以类比Thread
的isAlive()
,用于查询协程是否仍在执行。
Job
是一个接口类型,它具有以下三种状态:
状态 | 说明 |
---|---|
isActive |
活跃的。当Job 处于活动状态时为true ,如果Job 已经开始,但还没有完成、也没有取消或者失败,则是处于active 状态。 |
isCompleted |
已完成。当Job 由于任何原因完成时为true ,已取消、已失败和已完成Job 都是被视为完成状态。 |
isCancelled |
已退出。当Job 由于任何原因被取消时为true ,无论是通过显式调用cancel 或这因为它已经失败亦或者它的子或父被取消,都是被视为已退出状态。 |
这里模拟一个无限循环的协程,当协程是活跃状态时每秒钟打印两次消息,1.2秒后取消协程:
fun jobTest() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default){
var nextPrintTime = startTime
var i = 0
while (isActive) {//当job是活跃状态继续执行
if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
print("job: I'm sleeping ${i++} ...")
nextPrintTime += 500
}
}
}
delay(1200)//延迟1.2s
print("等待1.2秒后")
//job.join()
//job.cancel()
job.cancelAndJoin()//取消任务并等待任务完成
print("协程被取消并等待完成")
}
join()
是一个挂起函数,它需要等待协程的执行,如果协程尚未完成,join()
立即挂起,直到协程完成;如果协程已经完成,join()
不会挂起,而是立即返回。打印数据如下:
join.gif
Job
还可以有层级关系,一个Job
可以包含多个子Job
,当父Job
被取消后,所有的子Job
也会被自动取消;当子Job
被取消或者出现异常后父Job
也会被取消。具有多个子 Job
的父Job
会等待所有子Job
完成(或者取消)后,自己才会执行完成。
总的来说:它的作用是Job
实例作为协程的唯一标识,用于处理协程,并且负责管理协程的生命周期。
Deferred
继承自Job
,具有与Job
相同的状态机制。它是async
构建协程返回的一个协程任务,可通过调用await()
方法等待协程执行完成并获取结果。不同的是Job
没有结果值,Deffer
有结果值。
public interface Deferred<out T> : Job {
//等待协程执行完成并获取结果
public suspend fun await(): T
}
await()
: 等待协程执行完毕并返回结果,如果异常结束则会抛出异常;如果协程尚未完成,则挂起直到协程执行完成。T
: 这里多了一个泛型参数T
,它表示返回值类型,通过await()
函数可以拿到这个返回值。上面已有Deferred
代码演示,这里就不再重复实践。
通常我们提到的域
,都是用来描述范围的,域
既有约束作用又有提供额外能力的作用。
协程作用域(CoroutineScope
)其实就是为协程定义的作用范围 ,为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用CoroutineScope
的情况下启动新的协程。CoroutineScope
可被看作是一个具有超能力的ExecutorService
的轻量级版本。它能启动新的协程,同时这个协程还具备上面所说的suspend
和resume
的优势。
每个协程生成器launch
、async
等都是CoroutineScope
的扩展,并继承了它的coroutineContext
自动传播其所有元素和取消。协程作用域本质是一个接口:
public interface CoroutineScope {
//此域的上下文。Context被作用域封装,用于在作用域上扩展的协程构建器的实现。
public val coroutineContext: CoroutineContext
}
因为 启动协程需要作用域 ,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。
官方库给我们提供了一些作用域可以直接来使用:
runBlocking
:顶层函数,它的第二个参数为接收者是CoroutineScope
的函数字面量,可启动协程。但是它会阻塞当前线程,主要用于测试。GlobalScope
:全局协程作用域,通过GlobalScope
创建的协程不会有父协程,可以把它称为根协程
。它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。coroutineScope
:创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内。当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。为并行分解工作而设计的。supervisorScope
:与coroutineScope
类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。(作用域本身的失败(在block
或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。)MainScope
:为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main
,说明它是一个在主线程执行的协程作用域,通过cancel
对协程进行取消。推荐使用。fun scopeTest() {
//创建一个根协程
GlobalScope.launch {//父协程
launch {//子协程
print("GlobalScope的子协程")
}
launch {//第二个子协程
print("GlobalScope的第二个子协程")
}
}
//为UI组件创建主作用域
val mainScope = MainScope()
mainScope.launch {//启动协程
//todo
}
}
注意:MainScope
作用域的好处就是方便地绑定到UI组件的声明周期上,在Activity销毁的时候mainScope.cancel()
取消其作用域。
Android 官方对协程的支持是非常友好的,KTX 为 Jetpack 的Lifecycle
相关组件提供了已经绑定UV声明周期的作用域供我们直接使用:
lifecycleScope
:Lifecycle Ktx
库提供的具有生命周期感知的协程作用域,与Lifecycle
绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。viewModelScope
:与lifecycleScope
类似,与ViewModel
绑定生命周期,当ViewModel
被清除时,这个作用域将被取消。推荐使用。在build.gradle
添加Lifecycle相应基础组件后,再添加以下组件即可:
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
// 只有Lifecycles(没有 ViewModel 和 LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
因为Activity
实现了LifecycleOwner
这个接口,而lifecycleScope
则正是它的拓展成员,可以在Activity中直接使用lifecycleScope
协程实例:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btn_data.setOnClickListener {
lifecycleScope.launch {//使用lifecycleScope创建协程
//协程执行体
}
}
}
}
在ViewModel
中使用创建协程:
class MainViewModel : ViewModel() {
fun getData() {
viewModelScope.launch {//使用viewModelScope创建协程
//执行协程
}
}
}
注意:VIewModel 的作用域会在它的 clear 函数调用时取消。
官方框架在实现复合协程的过程中也提供了作用域,主要用于明确父子关系,以及取消或者异常处理等方面的传播行为。该作用域分为以下三种:
- 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
- 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
- 子协程会继承父协程的协程上下文中的元素,如果自身有相同
key
的成员,则覆盖对应的key
,覆盖的效果仅限自身范围内有效。
在上面介绍协程概念的时候,协程的挂起与恢复在哪挂起,什么时候恢复,为什么能切换线程,这因为调度器的作用:它确定相应的协程使用那些线程来执行。
CoroutineDispatcher
调度器指定指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
协程需要调度的位置就是挂起点的位置,只有当挂起点正在挂起的时候才会进行调度,实现调度需要使用协程的拦截器。调度的本质就是解决挂起点恢复之后的协程逻辑在哪里运行的问题。调度器也属于协程上下文一类,它继承自拦截器:
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
//询问调度器是否需要分发
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
//将可运行块的执行分派到给定上下文中的另一个线程上。这个方法应该保证给定的[block]最终会被调用。
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
//返回一个continuation,它封装了提供的[continuation],拦截了所有的恢复。
public final override fun interceptContinuation(continuation: Continuation<T>): Continuation
它是所有协程调度程序实现扩展的基类(我们很少会自己自定义调度器)。可以使用newSingleThreadContext
和newFixedThreadPoolContext
创建私有线程池。也可以使用asCoroutineDispatcher
扩展函数将任意java.util.concurrent.Executor
转换为调度程序。
Kotlin 提供了四个调度器,您可以使用它们来指定应在何处运行协程:
调度器模式 | 说明 | 适用场景 |
---|---|---|
Dispatchers.Default |
默认调度器,非主线程。CPU 密集型任务调度器,适合处理后台计算。 |
通常处理一些单纯的计算任务,或者执行时间较短任务比如:Json 的解析,数据计算等。 |
Dispatchers.Main |
UI 调度器, Andorid 上的主线程。 |
调度程序是单线程的,通常用于UI 交互,刷新等。 |
Dispatchers.Unconfined |
一个不局限于任何特定线程的协程调度程序,即非受限调度器。 | 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。 |
Dispatchers.IO |
IO 调度器,非主线程,执行的线程是IO 线程。 |
适合执行IO 相关操作,比如:网络处理,数据库操作,文件读写等。 |
所有的协程构造器(如launch
和async
)都接受一个可选参数,即 CoroutineContext
,该参数可用于显式指定要创建的协程和其它上下文元素所要使用的CoroutineDispatcher
。
fun dispatchersTest() {
//创建一个在主线程执行的协程作用域
val mainScope = MainScope()
mainScope.launch {
launch(Dispatchers.Main) {//在协程上下参数中指定调度器
print("主线程调度器")
}
launch(Dispatchers.Default) {
print("默认调度器")
}
launch(Dispatchers.Unconfined) {
print("任意调度器")
}
launch(Dispatchers.IO) {
print("IO调度器")
}
}
}
打印数据如下:
image.png
在 Andorid 开发中,我们常常在子线程中请求网络获取数据,然后切换到主线程更新UI。官方为我们提供了一个withContext
顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)
来创建一个在IO
线程池中运行的块。您放在该块内的任何代码都始终通过IO
调度器执行。由于withContext
本身就是一个suspend
函数,它会使用协程来保证主线程安全。
//用给定的协程上下文调用指定的挂起块,挂起直到它完成,并返回结果。
public suspend fun withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
block
中的代码会被调度到上面指定的调度器上执行,并返回结果值。这个函数会使用新指定的上下文的dispatcher
,将block
的执行转移到指定的线程中。它会返回结果, 可以和当前协程的父协程存在交互关系, 主要作用为了来回切换调度器 。
GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
val result: User = withContext(Dispatchers.IO) {//网络请求(IO 线程)
userApi.getUserSuspend("FollowExcellence")
}
tv_title.text = result.name //更新 UI(主线程)
}
在主线程中启动一个协程,然后再通过withContext(Dispatchers.IO)
调度到IO
线程上去做网络请求,获取结果返回后,主线程上的协程就会恢复继续执行,完成UI的更新。
由于withContext
可让在不引入回调的情况下控制任何代码行的线程池,因此可以将其应用于非常小的函数,如从数据库中读取数据或执行网络请求。一种不错的做法是使用withContext
来确保每个函数都是主线程安全的,那么可以从主线程调用每个函数。调用方也就无需再考虑应该使用哪个线程来执行函数了。您可以使用外部 withContext
来让 Kotlin 只切换一次线程,这样可以在多次调用的情况下,以尽可能避免了线程切换所带来的性能损失。
全部0条评论
快来发表一下你的评论吧 !