在程序运行过程中某些操作(像是:网络IO、文件IO、CPU或GUP计算密集型工作等等)可能会耗费大量的时间,在单线程的环境下可能会造成线程的阻塞,在他们完成之前没办去做其它事情。
使用传统方法的话,我们可能会选择使用多线程来解决这个问题,将这些耗时操作放置到新的线程中去执行,使主线程能够正常的运行。那么本文标题所提到的协程是怎么一回事呢?
协程可以看作是一个轻量级的线程,他不是由操作系统或是虚拟机来实现的,而是通过编译器。这意味着相对于线程,协程的开销更小。大家可以从下面的这个例子中感受一下
下面是一段Kotlin使用协程的代码,创建了100万个协程 (官方的例子是使用的100K,不过运行时间太短,不好截内存的使用情况)。
fun main(args: Array)= runBlocking { val jobs= List(1_000_000){ launch(CommonPool){ delay(10L) println(it) } } jobs.forEach { it.join() } }
内存使用情况
运行耗时:
然后是使用线程来进行实现的代码:
fun main(args: Array) { val threadList=List(1_000_000){ Thread{ Thread.sleep(10L) println(it) } } threadList.forEach { it.start();it.join() } }
内存使用情况:
运行耗时:
。。经过十来分钟的等待后,依旧没有运行完成,无奈放弃了。
使用线程的代码,占用的内存几乎是使用协程的两倍。而且从运行时间上看使用协程实现的程序话费的时间要远远低于线程的实现方式。
单从这两点来看,协程拥有更高的执行效率,占用更少的系统资源。
那么Kotlin中的协程是通过什么来实现异步操作的呢?它使用的是一种叫做 挂起 的机制。
协程的挂起几乎是没有损耗的,换种说法,就是不需要选择额外的上下文或是操作系统调用。 另外一点, 挂起能很大程度上被用户库给控制:我们可以决定在挂起状态下具体做些什么,并且围绕着需求进行优化/日志/拦截等操作。
协程不能随随便便就被挂起,只能在一个称为挂起点的地方,在这里会去调用特别标记的函数。这样的函数被称作 挂起函数,因为你调用他们会挂起一个协程(如果允许这次调用的话,库可以直接进行处理而不需要挂起)。 挂起函数的声明需要添加suspend修饰符。例如:
suspend fun doSomething(foo: Foo): Bar { ... }
挂起函数就像平常使用的函数一样,可以有参数和返回值,但是他们只能被协程或是其它挂起函数调用。事实上,要想启动一个协程,至少得有一个挂起函数,并且一般是匿名的(也就是一个挂起lambda表达式)。
线程往往是没有返回值(实现Runnable接口),尽管可以通过实现Callable接口来获得带返回值的线程。但这与协程在语法层面上的支持,在使用的便捷性上还是有不少差距的。
协程是通过编译技术实现的 (不需要虚拟机或操作系统的特别支持),这一点在开头也提到了。挂起操作通过代码变换实现。基本上,每一个挂起函数(可能会进行优化,但我们在着不想讨论这点)都被转换成一个状态机,那些状态与挂起调用相对应。在一个挂起准备好之前,下一状态与相关局部变量等一起存储在编译器生成的类的字段中。在恢复该协程时,恢复局部变量并且状态机从刚好挂起之后的状态进行。挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。
许多其它语言实现的异步机制也能制作成库,在Kotlin的协程中使用。包括:C#和ECMAScript写的 async/await , channels ;Go语言写的 select ;C#和Python写的 generators/yield 。
最后总结下协程相对与线程的优点:
轻量级,占用更少的系统资源; 更高的执行效率; 挂起函数较于实现Runnable或Callable接口更加方便可控; kotlin.coroutine 核心库的支持,让编写异步代码更加简单。 下一次,会具体的讲一讲如何在Kotlin中使用协程。