【KT-C-1】Kotlin 协程的建立方法

Tips 0: 为简化表述,本文中「协程」特指「Kotlin 协程」。web

协程是什么-ver2.0

上一篇文章作了不少空洞的理论阐述,来讲明协程在概念上应该如何理解,但咱们很难把概念直接用做开发实践的指导。因此正式使用协程以前,咱们还须要理解代码的世界中协程是什么。算法

Kotlin 面向对象的编程语言,线程能够直接对应成 Thread 对象,但协程中并无 Coroutine 类或接口给咱们使用,使用协程不是很贴近老 Java 人的直觉,是有必定学习成本的。(追到源码中看的话仍是能找到 AbstractCoroutine 的类型的,是标记为 InternalCoroutinesApi 的,不该该在外部使用)编程

在实际使用中,协程是一段代码,在挂起和恢复之间运行。协程的生命周期就是从挂起运行代码到正常恢复或者异常恢复结束的过程。json

协程的生命周期

粗略来看,协程有三种状态(协程运行的过程当中状态不常常改变,其实能够更加细分)安全

image.png

看起来有些麻烦,但也彻底不麻烦,随着使用的深刻,会天然而然地记住的。如今咱们只看如何建立协程,新建立的协程在 Incomplete 状态。markdown

建立协程

从 Hello World 开始,咱们都学会了 GlobalScope.launch { } 的方式建立一个协程,本节内容就从 launch 开始。并发

coroutineScope.launch 用于建立无返回值的协程,在代码执行结束后协程就自动结束了。launch 函数的返回值是 Job 类型,能够经过 Job 获取协程的状态、启动和取消协程以及监听协程执行完毕。app

coroutineScope.async 是另外一种建立协程的方式,用于建立有返回值的协程,须要主动调用 await() 获取结果后结束。async 的返回值是 Deferred<T 类型,比起 Job 增长了 await 函数。异步

launch 和 async 的参数相同,都是 CoroutineContext、CoroutineStart 和构成协程内容的 block。CoroutineContext 是协程运行环境,包含了各类协程须要的信息,暂且不展开叙述了。CoroutineStart 用来控制建立的协程如何启动,默认值的 CoroutineStart.DEFAULT 表示马上执行,因此大部分示例代码中并不须要主动调用 start 开始协程。async

举两个简单的栗子看一下吧,首先是一个很是有趣的排序算法「睡眠排序」的协程版本。

private fun sort(nums: IntArray) {
    nums.forEach {
        lifecycleScope.launch {
            delay(it * 10L) // 1ms 容易出现偏差,1s 又过久了,折中
            Log.w("CoroutineSampleActivity", "sort $it")
        }
    }
}

// test
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sort(intArrayOf(10, 4, 2, 1, 9, 3, 5, 8, 6, 7))
}
复制代码

image.png

我在循环中启动 N 个协程,替代了原算法中的建立 Thread。把 Thread.sleep() 优化成 delay() 应该有一个性能的突破,建议把协程版本命名为「延迟排序」。launch 建立的协程在代码执行完毕以后就不用管了,协程也不须要手动释放。

再看一个 async 的例子吧,此次选一个有用的实践代码,咱们读取一个 asset 的文件,获取内容字符串。文件 IO 属于耗时操做,不该该在 UI 线程进行,异步处理正是协程要解决的痛点,看代码以前能够先思考一下直接建立线程的写法应该是什么样的。

private suspend fun loadFile(assetName: String): String{
    val config = lifecycleScope.async(Dispatchers.IO) {
        val inputStream = assets.open(assetName)
        return@async inputStream.string() // 是自定义的普通扩展函数,与协程无关
    }
    return config.await()
}

// test
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch {
        val config = loadFile("config.json")
        Log.w("CoroutineSampleActivity", "loadFile $config")
    }
}
复制代码

image.png

文件是我随便写的,读到了就好。用协程代替回调和 handler 跨线程传消息,代码确实简洁了不少。

从实践中理解协程和线程

回顾上面的例子,代码到底在哪些线程呢?读文件的例子中,咱们为了避免妨碍 UI 线程指定了 CoroutineContext 为 Dispatchers.IO,排序的例子则没有明确提过线程,协程的内容代码究竟是在哪一个线程执行的呢?这个能够经过加 Log 的方式查看。

image.png

image.png

读文件在一个 worker 线程,其余代码都在 main 线程。看得出来,协程中的代码到底在哪一个线程执行实际上是由 Dispatcher 控制的,若是示例2的代码中不指定 Dispatchers.IO 的话就影响到 UI 线程了。

涉及并发编程的时候,咱们必须考虑线程安全的问题,在使用协程的时候也一样须要注意,敲代码的同时就要在内心梳理好每一行代码应该在哪一个线程执行。也就是说,使用协程并不意味着不须要理解线程原理。

在协程的官网介绍中,有一段关于协程「轻量」的描述:

import kotlinx.coroutines.*

//sampleStart
fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(5000L)
            print(".")
        }
    }
}
//sampleEnd
复制代码

意思是建立十万个协程同时运行也不会对系统形成负担,每一个协程都会在 delay 5 秒以后完成打印自动结束。若是同时建立十万个线程,就几乎不可能顺利运行。我目前并不承认官方的观点,这种对比的确体现了 Kotlin 中协程与线程的区别,只不过不太能证实「轻量」。

从上面实验和以前的理论知识来看,delay 不会阻塞当前线程,经过 launch 启动的十万协程 delay 后应该都在同一线程输出。咱们简化一下,就用 100 个协程加日志看看效果。

image.png

流畅运行。

回到原来的例子,100000 个线程和 2 个线程比较协程固然轻松取胜。但实际项目中没人这样滥用线程,实现一样的功能咱们也能够用 2 个线程实现,协程的优点仍是代码更好写。

中场休息

建立协程并执行已经能实现一些功能了,但还算不上可使用协程,上面简单略过的 CoroutineContext、CoroutineScope、Job 等都须要更加深刻理解。另外从实践代码中咱们都能明显看到 suspend 的地方,但还未遇到本应成对出现 resume,到底是怎么回事呢?欢迎关注后续文章。

(一直咕咕咕有点怕了本身了,尝试一个激励机制,点赞+评论超过 20 下一篇就在发布时间起 7 日内更新)