ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Kotlin协程的使用

2022-01-19 20:04:16  阅读:233  来源: 互联网

标签:suspend 协程 Kotlin Job CoroutineScope 使用 scope Dispatchers


前言

本篇是在Android官网对Kotlin协程的学习记录。记录Kotlin CoroutinesAndroid上的特点、应用等

协程概述

一、协程是什么?

协程是一种并发的设计模式,可以使用它来简化异步执行的代码,它可以帮助管理一些耗时的任务,以防耗时任务阻塞主线程。协程可以用同步的方式写出异步代码,代替了传统的回调方式,让代码更具有可读性。

二、协程的特点?

  • 轻量(Lightweight):其实这里的轻量是相对线程阻塞而言的,协程支持挂起,挂起的时候并不会阻塞当前线程,也就是"非阻塞式挂起",在协程挂起的时候线程可以做其它的事情,而线程的阻塞期间是无法做其他事情的。所以协程的"非阻塞式挂起"可以节省系统的资源。

  • 内存泄漏更少(Fewer memory leaks):用户关闭页面的时候,后台线程可能仍然有还在运行的任务,如果使用传统的线程进行后台请求,可能没有很好的办法让线程及时地停止运行,使用协程的话,可以通过Job::cancel让协程及时地停止运行,并且可以通过协程作用域CoroutineScope对协程进行统一管理,例如对通过CoroutineScope启动的协程统一进行cancel,这种就称作结构化并发,它让我们的程序有更少的协程泄漏,协程泄漏可以看做是一种内存泄露。

  • 内置取消支持(Built-in cancellation support):Cancellation会自动在运行中的整个协程层次结构内传播。

  • Jetpack和第三方框架支持:一些比如RoomViewModelJetpack组件,第三方框架Retrofit等有对Kt协程提供支持。

关于协程作用域:协程必须运行在CoroutineScope里(协程作用域),一个 CoroutineScope 管理一个或多个相关的协程。例如viewmodel-ktx包下面有 viewModelScopeviewModelScope管理通过它启动的协程,如果viewModel被销毁,那么viewModelScope会自动被取消,通过viewModelScope启动的正在运行的协程也会被取消。

挂起与恢复

协程有suspendresume两项概念:

  • suspend(挂起):暂停执行当前协程,并保存所有局部变量。
  • resume(恢复):用于让已挂起的协程从挂起处继续执行。

协程中有一个suspend关键字,它和刚刚提到的suspend概念要区分一下,刚刚提到的suspend(挂起)是一个概念,而suspend关键字可以修饰一个函数,但是仅这个关键字没有让协程挂起的作用,一般suspend关键字是提醒调用者该函数需要直接或间接地在协程下面运行,起到一个标记与提醒的作用。

suspend关键字的标记与提醒有什么作用?在以前,开发者很难判断一个方法是否是耗时的,如果错误地在主线程调用一个耗时方法,那么会造成主线程卡顿,有了suspend关键字,耗时函数的创建者可以将耗时方法使用suspend关键字修饰,并且在方法内部将耗时代码使用withContext{Dispatchers.IO}等方式放到IO线程等运行,开发者只需要直接或间接地在协程下面调用它即可,这样就可以避免耗时任务在主线程中运行从而造成主线程卡顿了。

下面通过官方的一个例子,对协程的suspendresume两个概念进行说明:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

我们假设在协程中调用fetchDocs方法,该协程提供了一个主线程环境(如启动协程时通过Dispatchers.Main指定),另外,get方法执行耗时任务,它使用挂起函数withContext{Dispatchers.IO}将耗时任务放到了IO线程中执行。

fetchDocs方法里,当执行到get方法开始进行网络请求的时候,它会挂起(suspend)所在的协程,当网络请求完成时,get会恢复(resume)已挂起的协程,而不是使用回调通知主线程

Kotlin使用栈帧(stack frame)管理正在运行的函数以及它的局部变量,当挂起一个协程的时候,系统会复制并保存当前的栈帧以供稍后使用。协程恢复时,会将栈帧从其保存位置复制回来,然后函数再次开始运行

调度器

Kotlin协程必须运行在dispatcher里面,协程可以将自己suspenddispatcher负责resume它们。

有下面三种Dispatcher

  • Dispatchers.Main:在主线程运行协程。
  • Dispatchers.IO:该dispatcher适合执行磁盘或网络I/O,并且经过优化。
  • Dispatchers.Default:该dispatcher适合执行占用大量 CPU 资源的工作(对列表排序和解析JSON),并且经过优化。

启动协程

有以下两种方式启动协程:

  • launch:启动新协程,launch的返回值为Job,协程的执行结果不会返回给调用方。
  • async:启动新协程,async的返回值为DeferredDeferred继承至Job,可通过调用Deferred::await获取协程的执行结果,其中await是挂起函数。

在一个常规函数启动协程,通常使用的是launch,因为常规函数无法调用Deferred::await,在一个协程或者挂起函数内部开启协程可以使用async

launchasync的区别:

  1. launch启动的协程没有返回结果;async启动的协程有返回结果。
  2. launch启动的协程有异常会立即抛出;async启动的协程的异常不会立即抛出,会等到调用Deferred::await的时候才将异常抛出。
  3. async适合于一些并发任务的执行,例如有这样的业务:做两个网络请求,等两个请求都完成后,一起显示请求结果。使用async可以这样实现
interface IUser {
    @GET("/users/{nickname}")
    suspend fun getUser(@Path("nickname") nickname: String): User

    @GET("/users/{nickname}")
    fun getUserRx(@Path("nickname") nickname: String): Observable<User>
}
val iUser = ServiceCreator.create(IUser::class.java)
GlobalScope.launch(Dispatchers.Main) {
    val one = async {
        Log.d(TAG, "one: ${threadName()}")
        iUser.getUser("giagor")
    }
    val two = async {
        Log.d(TAG, "two: ${threadName()}")
        iUser.getUser("google")
    }
    Log.d(TAG, "giagor:${one.await()} , google:${two.await()} ")
}

协程概念

CoroutineScope

CoroutineScope会跟踪它使用launchasync创建的所有协程,可以调用scope.cancel()取消该作用域下所有正在运行的协程。在ktx中,为我们提供了一些已经定义好的CoroutineScope,如ViewModelviewModelScopeLifecyclelifecycleScope,具体可以查看Android KTX | Android Developers

viewModelScope会在ViewModel的onCleared()方法中被取消

可以自己创捷CoroutineScope,如下:

class MainActivity : AppCompatActivity() {
    val scope = CoroutineScope(Job() + Dispatchers.Main)
    
    override fun onCreate(savedInstanceState: Bundle?) {
    	...
        scope.launch {
            Log.d(TAG, "onCreate: ${threadName()}") // main
            fetchDoc1()
        }
        
        scope.launch { 
            ...
        }
    }
    
    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {...}
    
    override fun onDestroy() {
        scope.cancel()
        super.onDestroy()
    }
}

创建scope的时候,将JobDispatcher联合起来,作为一个CoroutineContext,作为CoroutineScope的构造参数。当scope.cancel的时候,通过scope开启的所有协程都会被自动取消,并且之后无法使用scope来开启协程(不会报错但是协程开启无效)。

也可以通过传入CoroutineScopeJob来取消协程:

    val job = Job()
    val scope = CoroutineScope(job + Dispatchers.Main)

    scope.launch {...}
	...
	job.cancel()

使用Job取消了协程,之后也是无法通过scope来开启协程的。

其实查看源码,可以发现CoroutineScope.cancel方法内部就是通过Job进行cancel的:

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

关于协程的取消后面还会再进行介绍。

Job

当我们使用launch或者async创建一个协程的时候,都会获取到一个Job实例,这个Job实例唯一地标识这个协程,并且管理这个协程地生命周期。Job有点类似Java中的Thread类。

JavaThread类的部分方法:

image-20220101184731221

它可以对所创建的线程进行管理。

Job类还有部分扩展函数如下:

image-20220101185026302

CoroutineContext

CoroutineContext使用下面的几种元素定义了协程的行为:

  • Job:控制协程的生命周期。
  • CoroutineDispatcher:将工作分派到适当的线程。
  • CoroutineName:协程的名称,可用于调试。
  • CoroutineExceptionHandler:处理未捕获的异常。

对于在作用域内创建的新协程,系统会为新协程分配一个新的 Job 实例,而从包含协程的作用域继承其他 CoroutineContext 元素。可以通过向 launchasync 函数传递新的 CoroutineContext 替换继承的元素。请注意,将 Job 传递给 launchasync 不会产生任何效果,因为系统始终会向新协程分配 Job 的新实例。

例如:

val scope = CoroutineScope(Job() + Dispatchers.Main + CoroutineName("Top Scope"))

scope.launch(Dispatchers.IO) {
    Log.d(TAG, "onCreate: ${coroutineContext[CoroutineName]}")
}
D/abcde: onCreate: CoroutineName(Top Scope)

新创建的协程从外部的scope继承了CoroutineName等元素,但注意,CoroutineDispatcher元素被重写了,在新创建的协程里,CoroutineDispatcher元素被指定为Dispatchers.IO

避免使用GlobalScope

官方文档中,对于不提倡使用GlobalScope,给出了三个原因:

  • (一)Promotes hard-coding values. If you hardcode GlobalScope, you might be hard-coding Dispatchers as well.
  • (二)Makes testing very hard as your code is executed in an uncontrolled scope, you won't be able to control its execution.
  • (三)You can't have a common CoroutineContext to execute for all coroutines built into the scope itself.

关于第二点和第三点的解释如下:我们自己创建的CoroutineScope可以进行结构化并发的操作,例如我们可以调用CoroutineScope.cancel去取消该作用域下所有正在运行的协程,cancel方法如下:

public fun CoroutineScope.cancel(cause: CancellationException? = null) {
    val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
    job.cancel(cause)
}

它内部先获取CoroutineContextJob,然后第哦啊有Jobcancel方法,实现协程的取消。我们手动创建的CoroutineScopeCoroutineContext中都是有Job的,例如:

val scope = CoroutineScope(Job() + Dispatchers.Main + CoroutineName("Top Scope"))

它的构造方法为:

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

构造方法中,若传入的CoroutineContext没有Job,则会创建一个Job添加到CoroutineContext中。但是GlobalScope是全局(单例)的,它的CoroutineContext是一个EmptyCoroutineContext,里面没有Job成员

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

我们在调用GlobalScope.launch时,可以指定本次启动的协程的CoroutineContext。当我们在调用GlobalScope.cancel()的时候,会报下面的错误:

java.lang.IllegalStateException: Scope cannot be cancelled because it does not have a job: kotlinx.coroutines.GlobalScope@11b671b

可以看出,报错的原因就是因为GlobalScope没有Job

协程的取消

官方文档的原话:

Cancellation in coroutines is cooperative, which means that when a coroutine's Job is cancelled, the coroutine isn't cancelled until it suspends or checks for cancellation. If you do blocking operations in a coroutine, make sure that the coroutine is cancellable.

可以得出:

  1. 协程的取消是协作式
  2. 外部对当前正在运行的协程的取消,协程不会立即取消,当下面两种情况之一发生时,协程才会取消
    • 该协程的配合检查,协同进行取消,这和停止一个线程的执行类似(需要线程的配合检查)。
    • 当协程suspend的时候,协程也会被取消。

主动检查

举个例子:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
	scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
	}
}

bn2.setOnClickListener {
	scope.cancel()
}

假如我们只点击bn1开启协程,但是不点击bn2去取消协程,那么输出为

D/abcde: onCreate: true
D/abcde: onCreate: DefaultDispatcher-worker-1,Top Scope

假设我们点击bn1开启协程后,立即点击bn2取消协程(此时协程仍然在Thread.sleep期间),那么输出为

D/abcde: onCreate: false
D/abcde: onCreate: DefaultDispatcher-worker-2,Top Scope

可以看到,协程的isActive的值变为false,但是协程仍然会执行(虽然之后无法通过scope再去启动新的协程)。

上面的例子中,已经调用了scope.cancel,但是当前协程仍然还在运行,说明协程的真正取消需要协程内部的配合,其中一个方法就是调用ensureActive()函数,ensureActive的作用大致上相当于:

if (!isActive) {
    throw CancellationException()
}

我们修改下上面的例子:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
	scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        // 检查协程是否取消
        ensureActive()
        Log.d(TAG, "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
	}
}

bn2.setOnClickListener {
	scope.cancel()
}

我们点击bn1开启协程后,立即点击bn2取消协程(此时协程仍然在Thread.sleep期间),那么输出为

D/abcde: onCreate: false

可以看到,当前协程内部的ensureActive()函数配合外部的cancel操作,成功地将协程取消了。

当然,也可以通过其它的方式在协程内部进行协作式地取消操作。

协程挂起

外部对协程cancel之后,运行的协程被suspend的时候,协程也会被取消。

对上面的例子改造一下:

val scope = CoroutineScope(Job() + Dispatchers.IO + CoroutineName("Top Scope"))

bn1.setOnClickListener {
    scope.launch {
        Thread.sleep(2000)
        Log.d(TAG, "onCreate: $isActive")
        withContext(Dispatchers.Main) {
            Log.d(TAG, 
                  "onCreate: ${threadName()},${coroutineContext[CoroutineName]?.name}")
        }
    }    
}

bn2.setOnClickListener {
	scope.cancel()
}

假如我们只点击bn1开启协程,但是不点击bn2去取消协程,那么输出为

D/abcde: onCreate: true
D/abcde: onCreate: main,Top Scope

假设我们点击bn1开启协程后,立即点击bn2取消协程(此时协程仍然在Thread.sleep期间),那么输出为

D/abcde: onCreate: false

可以看出,withContextsuspend当前协程的时候,协程被取消了。

kotlinx.coroutines中的所有suspend函数都是可取消的(cancellable),例如withContext and delay(上面的例子中,不使用withContext,使用delay函数也是可以实现协程的取消的)。如果协程中调用了这些挂起函数,就不需要做任何其它的额外工作。

异常的处理

对于协程中的异常,可以使用try...catch...进行捕获,也可以使用CoroutineExceptionHandler

CoroutineExceptionHandlerCoroutineContext中的一种

协程中使用try...catch...捕获异常:

class LoginViewModel(
    private val loginRepository: LoginRepository
) : ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            try {
                loginRepository.login(username, token)
                // Notify view user logged in successfully
            } catch (error: Throwable) {
                // Notify view login attempt failed
            }
        }
    }
}

其它挂起函数

coroutineScope

挂起函数coroutineScope:创建一个CoroutineScope,并且在这个scope里面调用特定的suspend block,创建的CoroutineScope继承外部scopeCoroutineContextCoroutineContext中的Job会被重写)。

这个函数为parallel decomposition而设计,当这个scope的任何子协程fail,这个scope里面其它的子协程也会fail,这个scopefail了(感觉有点结构化并发的感觉)。

当使用coroutineScope的时候,外部的协程会被挂起,直到coroutineScope里面的代码和scope里面的协程运行结束的时候,挂起函数coroutineScope的外部协程就会恢复执行。

一个例子:

    GlobalScope.launch(Dispatchers.Main) {
        fetchTwoDocs()
        Log.d(TAG, "Under fetchTwoDocs()")
    }

    suspend fun fetchTwoDocs() {
        coroutineScope {
            Log.d(TAG, "fetchTwoDocs: ${threadName()}")
            val deferredOne = async {
                Log.d(TAG, "async1 start: ${threadName()}")
                fetchDoc1()
                Log.d(TAG, "async1 end: ${threadName()}")
            }
            val deferredTwo = async {
                Log.d(TAG, "async2: start:${threadName()}")
                fetchDoc2()
                Log.d(TAG, "async2 end: ${threadName()}")
            }
            deferredOne.await()
            deferredTwo.await()
        }
    }

    suspend fun fetchDoc1() = withContext(Dispatchers.IO) {
        Thread.sleep(2000L)
    }

    suspend fun fetchDoc2() = withContext(Dispatchers.IO) {
        Thread.sleep(1000L)
    }
D/abcde: fetchTwoDocs: main
D/abcde: async1 start: main
D/abcde: async2: start:main
D/abcde: async2 end: main
D/abcde: async1 end: main
D/abcde: Under fetchTwoDocs()

几个关注点:

  1. Under fetchTwoDocs()fetchTwoDocs执行完毕后才输出
  2. coroutineScope里面的代码在主线程运行
  3. async的代码运行在main线程中,因为coroutineScope创建的scope会继承外部的GlobalScope.launchCoroutineContext

上面的代码即使不调用deferredOne.await()deferredTwo.await(),也是一样的执行和输出结果。

suspendCoroutine

/**
 * Obtains the current continuation instance inside suspend functions and suspends
 * the currently running coroutine.
 *
 * In this function both [Continuation.resume] and [Continuation.resumeWithException] can be used either synchronously in
 * the same stack-frame where the suspension function is run or asynchronously later in the same thread or
 * from a different thread of execution. Subsequent invocation of any resume function will produce an [IllegalStateException].
 */
@SinceKotlin("1.3")
@InlineOnly
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

suspendCoroutine是一个主动挂起协程的行为,它会给你一个Continuation,让你决定什么时候去恢复协程的执行。

参考

  1. Kotlin coroutines on Android | Android Developers

标签:suspend,协程,Kotlin,Job,CoroutineScope,使用,scope,Dispatchers
来源: https://www.cnblogs.com/giagor/p/15823839.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有