关于Coroutine的一些基础知识

202 阅读7分钟

什么是Coroutine

回答这个问题我们首先需要知道关于函数和线程

函数是什么:

带有输入的一连串的指令执行后返回一个值

线程是什么:

函数执行的上下文(这里只是针对函数角度去定义线程,当然线程也有别的定义.)

如下我们有一个线程1

Thread 1

println("Hello World")
var x = 3
x *= x
println("The result is $x")

上面一段指令我们从上到下依次执行在同一个线程

现在有一个Thread 2

println("Hello World from the 2. Thread!")
var x = 3
x *= x
println("The result from 2. Thread is $x")

这个时候我们Thread 1和Thread 2执行顺序是独立的, 互相不影响,Thread 1执行到第二行,Thread 2可能还在第一行.

我们在Android中所有的UI相关绘制都是执行在Main Thread中的,如果出现耗时操作就会阻塞UI线程的绘制

instantiateView()
updateUI()

doNetworkCall() //耗时操作

updateUI()
println("Hello from the main thread")
updateUI()

对于这种情况我们另外启动一个新线程

instantiateViews()
updateUI()
Thread {
    doNetworkCall()
}.start()
updateUI()
println("Hello from the main thread")
updateUI()

我们网络请求结束回来又需要更新UI, 这个时候我们通常会使用Handler.

那么现在来回答什么是Coroutine?

它其实主要还是启动后台线程,能够解决卡UI的问题. 但是它相对于线程来说有一些区别

  • 它执行在一个线程里面
  • Coroutine是可以挂起的
  • Coroutine支持Context切换(解决网络请求结束回来又需要更新UI的场景)

创建你的第一个Coroutine

GlobalScope.launch {
    Log.d("tag", "Hello from thread ${Thread.currentThread().name}")
}
Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")

运行后输出:

D  Coroutine says hello from thread main
D  Hello from thread DefaultDispatcher-worker-2

suspend函数

kotlin协程库中提供了一个挂起函数delay

GlobalScope.launch {
    delay(1000) //挂起函数
    Log.d("tag", "Hello from thread ${Thread.currentThread().name}")
}
Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")

我们看到前面有suspend关键字代表了它是挂起函数

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
    }
}

挂起函数特点是它只能在挂起函数或者协程中调用,下面我们自定义一个挂起函数

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        GlobalScope.launch {
            val answer = doNetworkCall()
            Log.d("tag", answer)
        }
        Log.d("tag", "Coroutine says hello from thread ${Thread.currentThread().name}")
    }

    suspend fun doNetworkCall() : String {
        delay(3000L)
        return "This is the answer"
    }
}

Coroutine Context

通常Coroutine是执行在一个上下文当中的,我们可以通过设置上下文中的Dispacher去指定Coroutine执行的线程.

Dispatchers是一种CoroutineContext用来指定Coroutine在哪里运行,目前分别有四种

Dispatchers.Default

它使用共享后台线程的公共池。对于消耗 CPU 资源的计算密集型协程来说,这是一个合适的选择。

Dispatchers.IO

使用按需创建线程的共享池,专为密集型阻塞操作(如文件 I/O 和阻塞套接字 I/O)而设计。

Dispatchers.Unconfined

不限定线程池,Coroutine直接执行在当前调用帧,当出现suspend的时候,执行完suspend后又会以suspend的线程继续执行后续的逻辑。(通常不使用)

另外还可以创建自己的线程池执行

通过newSingleThreadContext 和 newFixedThreadPoolContext.

使用withContext可以切换上下文,用来在后台执行网络请求后刷新UI

GlobalScope.launch(Dispatchers.IO) {
    val answer = doNetworkCall()
    withContext(Dispatchers.Main) {
        Log.d("tag", "update ui $answer")
    }
}

runBlocking

runBlocking会运行一个阻塞当前线程运行的协程.

我们同样在主线程中调用GlobalScope.launch(Dispatchers.Main)和runBlocking有一些区别,如下

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    GlobalScope.launch(Dispatchers.Main) {
        
    }

    runBlocking { 
        
    }
}

GlobalScope.launch虽然也是启动一个协程在UI线程中运行,但是它不会阻塞后续命令的执行, runBlocking就会直接阻塞.

我们直接看下面一段程序

Log.d("tag", "Before runBlocking")
runBlocking {
    Log.d("tag", "Start of runBlocking")
    delay(5000)
    Log.d("tag", "End of runBlocking")
}
Log.d("tag", "after runBlocking")

最终输出是

D  Before runBlocking
D  Start of runBlocking
D  End of runBlocking
D  after runBlocking

从上面输出我们可以看出来runBlocking阻塞住了.

我们还可以在runBlocking中启动协程

Log.d("tag", "Before runBlocking")
runBlocking {
    launch(Dispatchers.IO) {
        delay(3000L)
        Log.d("tag","Finished IO Coroutine 1")
    }
    launch(Dispatchers.IO) {
        delay(3000L)
        Log.d("tag","Finished IO Coroutine 2")
    }
    Log.d("tag", "Start of runBlocking")
    delay(5000)
    Log.d("tag", "End of runBlocking")
}
Log.d("tag", "after runBlocking")

输出:

D  Before runBlocking
D  Start of runBlocking
D  Finished IO Coroutine 1
D  Finished IO Coroutine 2
D  End of runBlocking
D  after runBlocking

关于Job

每当我们启动一个协程后会返回一个Job对象

val job = GlobalScope.launch(Dispatchers.IO) {  }

job提供一些常用方法例如join.

val job = GlobalScope.launch(Dispatchers.IO) {  }

runBlocking {
    job.join() //等待协程执行完成
}

对上面程序做以下修改

val job = GlobalScope.launch(Dispatchers.IO) {
    repeat(5) {
        Log.d("tag", "Coroutine is still working")
        delay(1000L)
    }
}

runBlocking {
    delay(2000L)
    job.join()
    Log.d("tag", "Main Thread is continuing...")
}

输出

D  Coroutine is still working
D  Coroutine is still working
D  Coroutine is still working
D  Coroutine is still working
D  Coroutine is still working
D  Main Thread is continuing...

job的cancel

val job = GlobalScope.launch(Dispatchers.IO) {
    repeat(5) {
        Log.d("tag", "Coroutine is still working")
        delay(1000L)
    }
}

runBlocking {
    delay(2000L)
    job.cancel()
    Log.d("tag", "Main Thread is continuing...")
}

输出

D  Coroutine is still working
D  Coroutine is still working
D  Coroutine is still working
D  Main Thread is continuing...

这样我们成功了cancel了一个Coroutine. 但是cancel一个Coroutine有时候并不是像上面这么容易,因为我们上一个示例中Coroutine大多数时间是在delay,所以调用cancel()的Coroutine很快能够接收到通知。但是如果在Coroutine中正在进行一些复杂的运算。例如改成下面这样

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val job = GlobalScope.launch(Dispatchers.IO) {
            Log.d("tag", "Starting long running calculation...")
            repeat(5) {
                for (i in 30..40) {
                    Log.d("tag", "Result for i = $i : ${fib(i)}")
                }
            }
            Log.d("tag", "Ending long running calculation...")
        }

        runBlocking {
            delay(2000L)
            job.cancel()
            Log.d("tag", "Canceled job!")
        }
    }

    fun fib(n : Int) : Long {
        return if (n == 0) 0
        else if (n == 1) 1
        else fib(n -1) + fib(n - 2)
    }
}

输出

D  Result for i = 33 : 3524578
D  Result for i = 34 : 5702887
D  Result for i = 35 : 9227465
D  Result for i = 36 : 14930352
D  Result for i = 37 : 24157817
D  Canceled job!
D  Result for i = 38 : 39088169
D  Result for i = 39 : 63245986
D  Result for i = 40 : 102334155

我们看到实际上调用了cancel并没有实际取消掉Coroutine的执行.为什么会这样?

实际上在Coroutine它在不停的进行计算,根本没有时间去执行cancel的判断,这个时候我们需要手动加上

for (i in 30..40) {
    if(isActive) {
        Log.d("tag", "Result for i = $i : ${fib(i)}")
    }
}

withTimeout方法

我们通常取消一个Coroutine都是因为超时,所以这里有一个简单的withTimeout方法用于取消协程

val job = GlobalScope.launch(Dispatchers.IO) {
    Log.d("tag", "Starting long running calculation...")
    withTimeout(3000L) {
        for (i in 30..40) {
            if (isActive) {
                Log.d("tag", "Result for i = $i : ${fib(i)}")
            }
        }
    }
    Log.d("tag", "Ending long running calculation...")
}
2023-01-07 16:36:56.257 25494-25531 tag D  Starting long running calculation...
2023-01-07 16:36:56.265 25494-25531 tag D  Result for i = 30 : 832040
2023-01-07 16:36:56.272 25494-25531 tag D  Result for i = 31 : 1346269
2023-01-07 16:36:56.286 25494-25531 tag D  Result for i = 32 : 2178309
2023-01-07 16:36:56.312 25494-25531 tag D  Result for i = 33 : 3524578
2023-01-07 16:36:56.362 25494-25531 tag D  Result for i = 34 : 5702887
2023-01-07 16:36:56.414 25494-25531 tag D  Result for i = 35 : 9227465
2023-01-07 16:36:56.502 25494-25531 tag D  Result for i = 36 : 14930352
2023-01-07 16:36:56.647 25494-25531 tag D  Result for i = 37 : 24157817
2023-01-07 16:36:56.884 25494-25531 tag D  Result for i = 38 : 39088169
2023-01-07 16:36:57.254 25494-25531 tag D  Result for i = 39 : 63245986
2023-01-07 16:36:57.819 25494-25531 tag D  Result for i = 40 : 102334155
2023-01-07 16:36:57.819 25494-25531 tag D  Ending long running calculation...

Async和Await

通常我们在一个CoroutineScope中执行代码都是顺序执行的,但是有的场景我们希望它是并行执行的,例如现在有两个网络请求调用

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch(Dispatchers.IO) {
            val time = measureTimeMillis {
                val answer1 = networkCall1()
                val answer2 = networkCall2()
                Log.d("tag", "Answer1 is $answer1")
                Log.d("tag", "Answer1 is $answer2")
            }
            Log.d("tag","Request took $time ms.")
        }
    }

    suspend fun networkCall1() : String {
        delay(3000L)
        return "Answer 1"
    }

    suspend fun networkCall2() : String {
        delay(3000L)
        return "Answer 2"
    }
}

输出

D  Answer1 is Answer 1
D  Answer1 is Answer 2
D  Request took 6004 ms.

实际上它是串行执行的

我们现在希望把它改成并行执行.现在把代码执行方式换一下,我们在GlobalScope再去创建Scope执行协程.

GlobalScope.launch(Dispatchers.IO) {
    val time = measureTimeMillis {
        var answer1: String? = null
        var answer2: String? = null
        val job1 = launch {
            answer1 = networkCall1()
        }
        val job2  = launch {
            answer2 = networkCall2()
        }
        job1.join()
        job2.join()
        Log.d("tag", "Answer1 is $answer1")
        Log.d("tag", "Answer1 is $answer2")
    }
    Log.d("tag","Request took $time ms.")
}

最后运行

2023-01-07 17:05:22.400 25998-26035 tag D  Answer1 is Answer 1
2023-01-07 17:05:22.400 25998-26035 tag D  Answer1 is Answer 2
2023-01-07 17:05:22.400 25998-26035 tag D  Request took 3011 ms.

这样它们就是并行执行.

这里还有一种更为科学的方式,使用async和await.

GlobalScope.launch(Dispatchers.IO) {
    val time = measureTimeMillis {
        var answer1 = async {
            networkCall1()
        }
        var answer2= async {
            networkCall2()
        }
        Log.d("tag", "Answer1 is ${answer1.await()}")
        Log.d("tag", "Answer1 is ${answer2.await()}")
    }
    Log.d("tag","Request took $time ms.")
}

输出的结果和上面一致. 这样代码实现起来更加简洁,通常我们有返回值的异步操作都可以使用async.

lifecycleScope 和 viewModelScope

使用普通的CoroutineScope会存在一个问题,例如

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.btn_skip).setOnClickListener {
            //协程A
            GlobalScope.launch {
                while (true) {
                    delay(1000L)
                    Log.d("tag", "Still running...")
                }
            }
            //协程B
            GlobalScope.launch {
                delay(5000L)
                Intent(this@MainActivity, SecondActivity::class.java).also {
                    startActivity(it)
                    finish()
                }
            }
        }
    }
}

当我们点击按钮,启动一个新的一个Activity后,实际上协程A还是会继续运行, 这会造成内存泄漏. 如果我们换成lifecycleScope后,就避免了上面的现象.

lifecycleScope.launch {
    while (true) {
        delay(1000L)
        Log.d("tag", "Still running...")
    }
}

lifecycleScope能够感知生命周期,在Activity Destory后会取消Coroutine. 但是需要注意的是之前在Job章节中取消Coroutine提到,Coroutine在不停的进行密集型计算操作中,根本没有时间去执行cancel的判断,这个时候我们需要手动加上,isActive的判断.

viewModelScope实际上和lifecycleScope一样,只是它是绑定了viewmodel的生命周期.

深入理解Coroutine的Cancel和异常处理

lifecycleScope.launch {
    try {
        launch {
            throw Exception()
        }
    } catch (e : Exception) {
        println("Caught exception : $e")
    }
}

如果运行上面代码,你会发现try catch根本没有用,还是会抛出异常.

如果我们要try catch它,必须写成这样

lifecycleScope.launch {
    launch {
        try {
            throw Exception()
        } catch (e: Exception) {

        }
    }
}

实际上Coroutine对于异常是向上抛出的,例如

lifecycleScope.launch { //抛出 3
    try {
        launch { //抛出 2 
            launch { //抛出 1
                throw Exception() 
            }
        }
    } catch (e : Exception) {
        println("Caught exception : $e")
    }
}

如上,我们实际上并没有捕获异常,异常按照 1 -> 2 -> 3的顺序抛出.

如果我们使用async创建Coroutine会是怎么样的呢?

lifecycleScope.launch { //抛出 2
    val string = async { //抛出 1
        delay(500L)
        throw Exception("error")
        "Result"
    }
}

以上情况还是会抛出异常,如果改成下面这样,是不会出现异常的

lifecycleScope.async { // 等待用户消费(调用await)
    val string = async { //抛出 1
        delay(500L)
        throw Exception("error")
        "Result"
    }
}

实际上async只有在调用await才会抛出异常, 所以下面的代码依然会抛出异常.

val deferred = lifecycleScope.async {
    val string = async {
        delay(500L)
        throw Exception("error")
        "Result"
    }
}
lifecycleScope.launch { //抛出 2
    deferred.await() //抛出 1
}

只能使用如下方式捕获异常

val deferred = lifecycleScope.async {
    val string = async {
        delay(500L)
        throw Exception("error")
        "Result"
    }
}
lifecycleScope.launch {
    try {
        deferred.await()
    } catch (e : Exception) {
        e.printStackTrace()
    }
}

Coroutine标准的异常捕获方式

使用CoroutineExceptionHandler

val handler = CoroutineExceptionHandler { _, throwable ->
    println("Caught exception : $throwable")
}
lifecycleScope.launch(handler) {
    launch { 
        throw Exception("Error")
    }
}

CoroutineScope和SupervisorScope

val handler = CoroutineExceptionHandler { _, throwable ->
    println("Caught exception : $throwable")
}
CoroutineScope(Dispatchers.Main + handler).launch {
    launch {
        delay(300L)
        throw Exception("Coroutine 1 failed")
    }
    launch {
        delay(400L)
        println("Coroutine 2 finished")
    }
}

输出

Caught exception : java.lang.Exception: Coroutine 1 failed

可以看到Coroutine 1失败了,也导致了Coroutine 2不会执行. CoroutineScope的机制就是,其中一个Coroutine失败会导致其中所有的Coroutine都会取消执行.

val handler = CoroutineExceptionHandler { _, throwable ->
    println("Caught exception : $throwable")
}
CoroutineScope(Dispatchers.Main + handler).launch {
    SupervisorScope {
        launch {
            delay(300L)
            throw Exception("Coroutine 1 failed")
        }
        launch {
            delay(400L)
            println("Coroutine 2 finished")
        }
    }
}

输出

I  Caught exception : java.lang.Exception: Coroutine 1 failed
I  Coroutine 2 finished

可以看到使用SupervisorScope并没有出现一个Coroutine失败会导致其中所有的Coroutine都会取消执行.

关于viewModelScope

viewModelScope实际上也是一个SupervisorScope,所以它里面的子协程是独立失败的,互相不影响

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        //使用了SupervisorJob
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }