Kotlin的协程

1,081 阅读4分钟

Kotlin的协程

一.协程是什么?

协程是一个线程框架,支持异步同步化,同时它又是结构化编程,便于跟踪、销毁以及追踪问题。

1.协程的挂起与恢复

挂起与阻塞不同,阻塞会阻塞当前线程,需要等待阻塞结束才能响应,而挂起并不阻塞当前线程,只是会在协程中执行,而当挂起函数执行完成后又会恢复执行。协程的挂起和恢复依赖于协程的基础框架中Continuation这个接口,你可以理解为普通线程中的回调,它的挂起和恢复是基于cps与状态机机制。

二.协程的使用

1.协程的启动

  • runBlocking:T

    顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T,一般在项目中不会使用,主要是为main函数和测试设计的。

  • launch

    创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。它返回的是一个该协程任务的引用,即Job对象。这是最常用的用于启动协程的方式。

  • async

     创建一个新的协程,不会阻塞当前线程,必须在协程作用域中才可以调用。并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果。常用于并发执行-同步等待和获取返回值的情况。

2.协程的取消

  • 协程的取消

    协程取消不会立即执行,在使用协程处理了一些相对较为繁重的工作,比如读取多个文件,不会立即停止此任务的进行。所有要加上一个isActive来判断下协程的状态 这个也是协程的状态检测 从Job的生命周期也能跟踪到;

    并且如果你想在协程取消后,在挂起函数中做清理工作,可以在final中去调用withContext(NonCancellable)就可以在其中执行挂起函数了。

  fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val launch = launch(Dispatchers.Default) {
        println("coroutine is launch ${Thread.currentThread().name}")
        try {
            launch {
                var nextPrintTime = startTime
                var j = 0
                while (isActive){
                    if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
                        print("job 1->>>>: I'm sleeping ${j++} ...")
                        nextPrintTime += 1000
                    }
                }
            }
            var nextPrintTime = startTime
            var i = 0
            while (i < 5 && isActive) {//打印前五条消息
​
                if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
                    print("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 1000
                }
            }
        } catch (e: CancellationException) {
            println(e)
        } finally {
            println("final...")
            //如果协程被取消后需要调用挂起函数进行清理任务,
            // 可使用NonCancellable单例对象用于withContext函数创建一个无法被取消的协程作用域中执行。
            // 这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。
            withContext(NonCancellable){
                delay(100)
            }
​
        }
    }
    delay(200)
    launch.cancel()
    println("coroutine is canceled")
​
}

3.协程的异常捕获

1.try catch使用

不要用 try-catch 直接包裹 launch、async

fun main() {
    runBlocking {
        printMsg("start")
        try {
            printMsg("try start")
            launch {
                printMsg("launch start")
                delay(200L)
                1 / 0          <------------200毫秒后创建一个异常
                printMsg("launch end")
            }
            printMsg("try end")
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }
        printMsg("end")
    }
}
​
//日志
main @coroutine#1 start
main @coroutine#1 try start
main @coroutine#1 try end
main @coroutine#1 end
main @coroutine#2 launch start
Exception in thread "main" java.lang.ArithmeticException: / by zero    <-----报错程序崩溃

虽然try-catch包裹了协程的内容,但是程序还是报错,这是因为子协程与父协程是并发执行的,它们之间是独立的执行流程,所以上面代码中父协程的 try-catch 无法捕获子协程抛出的异常。

fun main() {
    runBlocking {
        printMsg("start")
        launch {
            printMsg("launch start")
            try {
                printMsg("try start")
                delay(200L)
                1 / 0
                printMsg("try end")
            } catch (exception: Exception) {
                printMsg("catch $exception")
            }
            printMsg("launch end")
        }
        printMsg("end")
    }
}
​
//日志
main @coroutine#1 start
main @coroutine#1 end
main @coroutine#2 launch start
main @coroutine#2 try start
main @coroutine#2 catch java.lang.ArithmeticException: / by zero    <------异常被成功捕获
main @coroutine#2 launch end
Process finished with exit code 0

如果使用async创建协程,try-catch是应该包裹async内的代码块还是应该包裹deferred.await()? 写段代码看看

fun main() {
    runBlocking {
        printMsg("start")
        val deferred = async() {
            printMsg("async start")
            delay(200L)
            1 / 0
            printMsg("async end")
        }
​
        try {
            deferred.await()
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }
    
        printMsg("end")
    }
​
}
​
//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero     <------捕获到了异常
main @coroutine#1 end
Exception in thread "main" java.lang.ArithmeticException: / by zero   <-----报错程序崩溃

虽然捕获到了异常,但是程序还是报错了,所以try-catch一般还是包裹具体的代码块吧。

2.SupervisorJob的使用

上面的一段代码try-catchdeferred.await()仍然报错,有没有办法补救这段代码呢?答案是有,可以使用SupervisorJob()。代码如下:

fun main() {
    runBlocking {
        printMsg("start")
        val deferred = async(SupervisorJob()) {      <-------变化在这里
            printMsg("async start")
            delay(200L)
            1 / 0
            printMsg("async end")
        }
​
        try {
            deferred.await()
        } catch (exception: Exception) {
            printMsg("catch $exception")
        }
    
        printMsg("end")
    }
​
}
​
//日志
main @coroutine#1 start
main @coroutine#2 async start
main @coroutine#1 catch java.lang.ArithmeticException: / by zero
main @coroutine#1 end
Process finished with exit code 0

为什么加了SupervisorJob()就不报错了? 看下SupervisorJob()的源码:

@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
​
public interface CompletableJob : Job {
    
​
    public fun complete(): Boolean
    
    public fun completeExceptionally(exception: Throwable): Boolean
​
}

SupervisorJob() 其实不是构造函数,它只是一个普通的顶层函数。而这个方法返回的对象,是 Job 的子类。默认的 Job 类型会将异常传播给父协程,如果一个子协程抛出异常,它会取消父协程及其所有兄弟协程。

通过使用 SupervisorJob,我们可以创建一个具有独立异常处理行为的作业层级。这意味着即使子协程中发生异常,父协程仍然可以继续执行而不会被取消,从而避免整个程序崩溃。

SupervisorJob()可以作为 CoroutineScope 的上下文,但是它的监管范围并不是无限大的,看下面的例子:

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()    
        val scope = CoroutineScope(coroutineContext + supervisorJob)    <-----作用域内使用SupervisorJob()
        val job = scope.launch {              <----注意这里,作用域内启动子协程
            launch {                   <----注意这里,作用域内启动孙协程
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")      <----关注这个日志
            }
        }
        job.join()
        scope.cancel()
    }
}
​
//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main @coroutine#4" java.lang.ArithmeticException: by zero
Process finished with exit code 0

上面的日志中并没有输出job2 end,说明上面job1的异常影响了下面协程job2的执行,那如何修改呢?

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()
        val scope = CoroutineScope(coroutineContext + supervisorJob)
        scope.apply {               <----------变化在这里,launch改为apply
            val job1 = launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            val job2 = launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
            job1.join()       <----------变化在这里
            job2.join()
        }
        scope.cancel()
    }
}
​
//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end        <--------成功输出: job2 end
Process finished with exit code 0

可以看到当将 SupervisorJob 作为 CoroutineScope 的上下文时,它的监管范围仅限于该作用域内部启动的子协程。

SupervisorJob的源码中是因为重写了childCancelled方法并直接返回false,保证异常不会向父协程和其他子协程传递:

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

事实上kotlin有提供给我们含SupervisorJob上下文的协程作用域,它就是supervisorScope,源码如下:

/**
​
 * Creates a [CoroutineScope] with [SupervisorJob] and calls the specified suspend block with this scope.
 * The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
 * context's [Job] with [SupervisorJob].
 * This function returns as soon as the given block and all its child coroutines are completed.
   *
 * Unlike [coroutineScope], a failure of a child does not cause this scope to fail and does not affect its other children,
 * so a custom policy for handling failures of its children can be implemented. See [SupervisorJob] for additional details.
 * A failure of the scope itself (exception thrown in the [block] or external cancellation) fails the scope with all its children,
 * but does not cancel parent job.
   *
 * The method may throw a [CancellationException] if the current job was cancelled externally,
 * or rethrow an exception thrown by the given [block].
   */
   public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
   contract {
       callsInPlace(block, InvocationKind.EXACTLY_ONCE)
   }
   return suspendCoroutineUninterceptedOrReturn { uCont ->
       val coroutine = SupervisorCoroutine(uCont.context, uCont)      <-------SupervisorCoroutine
       coroutine.startUndispatchedOrReturn(coroutine, block)
   }
   }
​
private class SupervisorCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
    override fun childCancelled(cause: Throwable): Boolean = false    <-------同样重写了childCancelled方法返回false
}

3.supervisorScope的使用

我们使用supervisorScope改造上面的代码:

fun main() {
    runBlocking {
        supervisorScope {
            val job1 = launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            val job2 = launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
            job1.join()
            job2.join()
        }
    }
}
​
//日志
main @coroutine#2 job1 start
main @coroutine#3 job2 start
Exception in thread "main @coroutine#2" java.lang.ArithmeticException: by zero
main @coroutine#3 job2 end        <--------成功输出: job2 end
Process finished with exit code 0

4.CoroutineExceptionHandler

有时候由于协程嵌套的层级很深,并且也不需要每一个协程去处理异常,这时候CoroutineExceptionHandler就可以派上用场了,如下:

fun main() {
    runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineExceptionHandler)
        val job = scope.launch {
            launch {
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
        }
        job.join()
        scope.cancel()
    }
}
​
//日志
DefaultDispatcher-worker-2 @coroutine#3 job1 start
DefaultDispatcher-worker-3 @coroutine#4 job2 start
DefaultDispatcher-worker-3 @coroutine#4 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
Process finished with exit code 0

CoroutineExceptionHandler中成功输出了异常的日志。试试把CoroutineExceptionHandler放在子协程报错的地方有什么样的结果?

fun main() {
    runBlocking {
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineContext)       <--------变化在这里
        val job = scope.launch {
            launch(coroutineExceptionHandler) {       <--------变化在这里
                printMsg("job1 start")
                delay(200L)
                throw  ArithmeticException("by zero")
            }
            launch {
                printMsg("job2 start")
                delay(300L)
                printMsg("job2 end")
            }
        }
        job.join()
        scope.cancel()
    }
}
​
//日志
main @coroutine#3 job1 start
main @coroutine#4 job2 start
Exception in thread "main" java.lang.ArithmeticException: by zero      <-------程序报错
Process finished with exit code 1

程序报错,且coroutineExceptionHandler并没有捕获到异常,说明coroutineExceptionHandler并没有起到作用,原因是CoroutineExceptionHandler 只在顶层的协程当中才会起作用,当子协程当中出现异常以后,它们都会统一上报给顶层的父协程,然后由顶层的父协程去调用 CoroutineExceptionHandler来处理异常。

看上面的日志都没有输出job2 end,说明job1的异常影响到了job2的执行,那如果既想用coroutineExceptionHandler兜底异常,又不想协程间因为异常互相影响怎么办呢? 我们可以试试这样写:

fun main() {
    runBlocking {
        val supervisorJob = SupervisorJob()        <----------使用SupervisorJob()
        val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            printMsg("CoroutineExceptionHandler $throwable")
        }
        val scope = CoroutineScope(coroutineExceptionHandler + supervisorJob)    <-------加入到作用域的上下文
        scope.apply {
            val job1 = launch {
                printMsg("job1 start")
                delay(100L)
                throw  NullPointerException("parameters is null")     <-----子协程的异常
            }
​
            val job2 = launch {
                printMsg("job2 start")
                delay(200L)
                launch {                 <-----孙协程
                    try {
                        1 / 0            <-----孙协程的异常
                    } catch (exception: ArithmeticException) { 
                        throw  ArithmeticException("by zero")     <------记得抛出来,不抛出来也没有的
                    }
                }
            }
    
            val job3 = launch {
                printMsg("job3 start")
                delay(300L)
                printMsg("job3 end")
            }
    
            job1.join()
            job2.join()
            job3.join()
        }
        scope.cancel()
    }
​
}
​
//日志
DefaultDispatcher-worker-1 @coroutine#2 job1 start
DefaultDispatcher-worker-2 @coroutine#3 job2 start
DefaultDispatcher-worker-3 @coroutine#4 job3 start
DefaultDispatcher-worker-2 @coroutine#2 CoroutineExceptionHandler java.lang.NullPointerException: parameters is null
DefaultDispatcher-worker-3 @coroutine#5 CoroutineExceptionHandler java.lang.ArithmeticException: by zero
DefaultDispatcher-worker-3 @coroutine#4 job3 end
Process finished with exit code 0

总结来源:BILIBILI 动脑学院 扔物线 blog.csdn.net/star_nwe/ar…