接触新概念,最好的办法就是先总体看个大概,再回过头来细细品味。前端
文中若是没有特别说明,协程指编程语言级别的协程,线程则特指操做系统内核线程。编程
Kotlin 的协程从 v1.1 开始公测(Experimental) 到如今,已经算是很是成熟了,但你们对它的见解却一直存在各类疑问,为何呢?由于即使咱们把 Kotlin 丢掉,单纯协程这个东西自己就已经长时间让你们感到疑惑了,不信的话能够单独搜一下协程或者 Coroutine,甚至连 Lua 之父在提到为何协程鲜见于早期语言实现,就是由于这概念没有一个清晰的界定。缓存
更有意思的是,在查阅资料的过程当中,你会常常会陷入一种一下子『啊,我懂了』,一下子『啊,我懂个屁』的循环当中,不瞒各位说,我从七八年前刚开始学 Lua 的时候面对 Lua 的协程也是这个破感受,后来接触 goroutine 又来了一遍,接触 Kotlin 的协程又来了一遍,习惯就好。安全
那么咱们再来理一理协程的概念:微信
关键核心就是协程是一个能挂起而且待会儿恢复执行的东西。任什么时候候本身产生疑惑的时候都回过来再想一想这几句话,就算协程最终呈现给咱们的样子可能『花里胡哨』,但万变不离其宗。多线程
有的朋友不理解什么叫挂起,挂起这个词其实还真是源于操做系统的叫法,直观的理解上,你就当作暂停理解吧。并发
咱们前面提到,协程的概念其实并不混乱,那么混乱的是什么?是各家对它的实现。这就好像牛顿第二定律同样,看似很简单,F = ma,用起来就五花八门了,衍生的各类公式更是层出不穷。框架
协程不就是要挂起、恢复么,请问挂起恢复具体要怎么作?没有定义呀。既然没有定义是否是就能够随便?是的,抓住老鼠就是好猫~less
协程这一点儿跟线程真的是无法比啊,主流操做系统都有成熟的线程模型,应用层常常提到的线程的概念大多就是映射方式的差别,因此不一样的编程语言一旦引入了线程,那么基本上就是照搬了系统线程的概念,线程自己也不是他们实现的——这很好理解,由于线程调度是操做系统作的嘛。异步
Java 对线程作了很好的支持,这也是 Java 在高并发场景风生水起的一个关键支柱,不过若是你有兴趣去看下虚拟机底层对线程的支持,例如 Android 虚拟机,其实就是 pthread。Java 的 Object 还有一个 wait 方法,这个方法几乎支撑了各类锁的实现,它底层是 condition。
绝大多数协程都是语言层面本身的实现,不一样的编程语言有不一样的使用场景,天然在实现上也看似有很大的差别,甚至还有的语言本身没有实现协程,但开发者经过第三方框架的方式提供了协程的能力,例如 Java 的框架 Quasar(docs.paralleluniverse.co/quasar/),加上协程实现自己在操做系统层面就有过一系列演进,所以出现了虽然理论上看起来很简单,但实现上却多样化的局面。
咱们在前面讲各个语言的实现有差别,说的是看似有很大的差别,主要是各自的关键字、类型命名不同,但总结下来你们对于协程的分类更倾向于按照有没有栈来分,即:
栈这个东西你们应该都很熟悉了,咱们递归调用函数的层次太多就会致使 StackOverflowException
,由于栈内存是有限的;咱们的程序出现了异常咱们老是但愿看到异常点的调用关系,这样方便定位问题,这也须要栈。
有栈协程有什么好处呢?由于有栈,因此在任何一个调用的地方运行时均可以选择把栈保存起来,暂停这个协程,听起来就跟线程同样了,只不过挂起和恢复执行的权限在程序本身,而不是操做系统。缺点也是很是明显的,每建立一个协程无论有没有在运行都要为它开辟一个栈,这也是目前无栈协程流行的缘由。
goroutine 看上去彷佛不像协程,由于开发者本身没法决定一个协程的挂起和恢复,这个工做是 go 运行时本身处理的。为了支持 goroutine 在任意位置能挂起,goroutine 实际上是一个有栈协程,go 运行时在这里作了大量的优化,它的栈内存能够根据须要进行扩容和缩容,最小通常为内存页长 4KB。
JavaScript、C# 还有 Python 的协程,或者干脆就说 async/await,相比之下就轻量多了,它们看起来更像是针对回调加了个语法糖的支持——它们其实就是无栈协程的实现了。无栈,顾名思义,每个协程都不会单独开辟调用栈,那么问题来了,它的上下文是如何保存的?
这就要提到传说中的 CPS 了,即 continuation-passing-style。咱们来想象一下,程序被挂起,或者说中断,最关键的是什么?是保存挂起点,或者中断点,对于线程被操做系统中断,中断点就是被保存在调用栈当中的,而咱们的无栈协程要保存到哪儿呢?保存到 Continuation 对象当中,这个东西可能在不一样的语言当中叫法不同,但本质上都是一个 Continuation,它就是一个普通的对象,占用内存很是小,仍是很抽象是吧,想一想你常见的 Callback,它其实就是一个 Continuation 的实现。
Kotlin 的协程的根基就是一个叫作 Continuation 的类。我在前面的文章不止一次提到,这家伙长得横看竖看就是一个回调,resume 就是 onSuccess,resumeWithException 就是 onFailure。
Continuation 携带了协程继续执行所须要的上下文,同时它本身又是挂起点,由于待会儿恢复执行的时候只须要执行它回调的函数体就能够了。对于 Kotlin 来说,每个 suspend
函数都是一个挂起点,意味着对于当前协程来讲,每遇到一个 suspend
函数的调用,它都有可能会被挂起。每个 suspend
函数都被编译器插入了一个 Continuation 类型的参数用来保存当前的调用点:
suspend fun hello() = suspendCoroutine<Int>{ continuation ->
println("Hello")
continuation.resumeWith(Result.success(10086))
}
复制代码
咱们定义了一个 suspend
函数 hello
,它看起来没有接收任何参数,若是真是这样,请问咱们在后面调用 resumeWith
的 continuation
是哪里来的?
都说挂起函数必须在协程内部调用,其实也不是,咱们在前面讲挂起原理的时候就用 Java 代码直接去调用 suspend
函数,你们也会发现这些 suspend
函数都须要传入一个额外的 Continuation
,就是这个意思。
固然,Java 也不是必须的,咱们只须要用点儿 Kotlin 反射,同样能够直接让 suspend 函数现出原形:
val helloRef = ::hello
val result = helloRef.call(object: Continuation<Int>{
override val context = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>{
pritnln("resumeWith: ${result.getOrNull()}")
})
})
复制代码
这与咱们在协程挂起原理那篇的作法一模一样,咱们虽然没有办法直接调用 hello()
,但咱们能够拿到它的函数引用,用发射调用它(这个作法后续可能也会被禁掉,但 1.3.50 目前仍然是可用的),调用的时候若是你什么参数都不传,编译器就会提示你它须要一个参数,呃,你看,它这么快就投降了——须要的这个参数正是 Continuation
。
再强调一下,这段代码不须要运行在协程体内,或者其余的 suspend
函数中。如今请你们仔细想一想,为何官方要求 suspend
函数必定要运行在协程体内或者其余 suspend
函数中呢?
答案天然就是任何一个协程体或者 suspend
函数中都有一个隐含的 Continuation
实例,编译器可以对这个实例进行正确传递,并将这个细节隐藏在协程的背后,让咱们的异步代码看起来像同步代码同样。
说到这里,咱们已经接近 Kotlin 协程的本质了,它是一种无栈协程实现,它的本质就是一段代码 + Continuation 实例。
这个说法实际上是很奇怪的。我若是问你线程实际上是一个 CPU 框架吗,你确定会以为这俩,啥啊???
Kotlin 协程确实在实现的过程当中提供了切线程的能力,这是它的能力,不是它的身份,就比如拿着学位证非说这是身份证同样,学位证描述的是这人能干啥,不能描述这人是谁。
杠精们可能会说学位证有照片有名字啊。你拿着学位证去买飞机票你看人家认不认呗。
协程的世界能够没有线程,若是操做系统的 CPU 调度模型是协程的话;反过来也成立——这个应该不会有人反对吧。Kotlin 协程是否是能够没有线程呢?至少从 Java 虚拟机的实现上来看,好像。。。。不太行啊。没错,是不太行,不过这不是 Kotlin 协程的问题,是 Java 虚拟机的问题,谁让 Java 虚拟机的线程用起来没有那么难用呢,在它刚出来的时候简直吊打了当时其余语言对并发的支持(就像 goroutine 出来的时候吊打它同样)。
咱们知道 Kotlin 除了支持 Java 虚拟机以外,还支持 JavaScript,还支持 Native。JavaScript 不管是跑在 Web 仍是 Node.js 当中,都是单线程玩耍的;Kotlin Native 虽然能够调用 pthread,但官方表示咱们有本身的并发模型(Worker),不建议直接使用线程。在这两个平台上跑,Kotlin 的协程其实都是单线程的,又怎么讲是个线程框架呢?
说到这儿可能又有人有疑问了,单线程要协程能作什么呢?这个前端同窗可能会比较有感触,谁跟大家说的异步必定要多线程。。Android 开发的同窗其实能够想一想你在 Activity
刚建立的时候想要拿到一个 View 的大小通常返回都是 0,由于 Activity
的布局是在 onResume
方法调用以后完成的,因此 handler.post
一下就行了:
override fun onResume(){
super.onResume()
handler.post{
val width = myView.width
...
}
}
复制代码
这就是异步代码嘛,但这代码其实都运行在主线程的,咱们固然能够用协程改写一下:
override fun onResume() {
super.onReusme()
GlobalScop.launch(Dispatchers.Main) {
val width = hadler.postSuspend {
myView.width
}
Log.d("MyView",widht.toString())
}
}
suspend fun <T> Handler.postSuspend(block: () -< T) = suspendCoroutine<T> {
post {
it.resume(block())
}
}
复制代码
其实我我的以为若是 Kotlin 协程的默认的调度器是 Main
,而且这个 Main
会根据各自平台选择一个合适的事件循环,这样更能体现 Kotlin 协程在不一样平台的一致性,例如对于 Android 来讲 Main
就是 UI 线程上的事件循环,对于 Swing 一样是 Swing 的 UI 事件循环,只要是有事件循环的平台就默认基于这个循环来一个调度器,没有默认事件循环的也好办,Kotlin 协程自己就有 runBlocking
嘛,对于普通 Java 程序来讲没有事件循环就给它构造一个就好了。
Kotlin 协程的设计者没有这样作,他们固然也有他们的道理,毕竟他们不肯意强迫开发者必定要用协程,甚至马上立刻就得对原有的代码进行改造,他们但愿 Kotlin 只是一门编程语言,一门提供足够安全保障和灵活语法的编程语言,剩下的交给开发者去选择。
这可不是一个很容易回答的问题。
Kotlin 协程刚出来的时候,有人就作过性能对比,以为协程没有任何性能优点。咱们彻底能够认为他的测试方法是专业的,在一些场景确实用协程不会有任何性能上的优点,这就比如咱们须要在一个单核 CPU 上跑一个计算密集型的程序还要开多个线程跑同样,任何特性都有适合它的场景和不适合它的领域。
想必你们看各种讲解协程的文章都会提到协程比线程轻量,这个其实咱们前面也解释过了,编程语言级别实现的协程就是程序内部的逻辑,不会涉及操做系统的资源之间的切换,操做系统的内核线程天然会重一些,且不说每建立一个线程就会开辟的栈带来的内存开销,线程在上下文切换的时候须要 CPU 把高速缓存清掉并从内存中替换下一个线程的内存数据,而且处理上一个内存的中断点保存就是一个开销很大的事儿。若是没有直观的感觉的话,就尽情想象一下你正要拿五杀的时候公司领导在微信群里发消息问你今天的活跃怎么跌了的场景。
线程除了包含内核线程自己执行代码能力的含义之外,一般也被赋予了逻辑任务的概念,因此协程是一种轻量级的『线程』的说法,更多描述的是它的使用场景,这句话也许这样说更贴切一些:
协程更像一种轻量级的『线程』。
线程天然能够享受到并行计算的优待,协程则只能依赖程序内部的线程来实现并行计算。协程的优点其实更可能是体如今 IO 密集型程序上,这对于 Java 开发者来讲可能又是一个很迷惑的事情,由于你们写 Java 这么多年,不多有人用上 NIO,绝大多数都是用 BIO 来读写 IO,所以无论开线程仍是开协程,读写 IO 的时候老是要有一个线程在等待 IO,因此看上去彷佛也没有什么区别。但用 NIO 就不同了,IO 不阻塞,经过开一个或不多的几个线程来 select IO 的事件,有 IO 事件到达时再分配相应的线程去读写 IO,比起传统的 IO 就已经有了很大的提高。
欸?没有写错吗?你写的但是线程啊?
对啊,用了 NIO 之后,自己就能够减小线程的使用,没错的。但是协程呢?协程能够基于这个思路进一步简化代码的组织,虽然线程就能解决问题,但写起来实际上是很累的,协程可让你更轻松,特别是遇到多个任务须要访问公共资源时,若是每一个任务都分配一个线程去处理,那么少不了就有线程会花费大量的时间在等待获取锁上,但若是咱们用协程来承载任务,用极少许的线程来承载协程,那么锁优化就变得简单了:协程若是没法获取到锁,那么协程挂起,对应的线程就可让出去运行其余协程了。
我更愿意把协程做为更贴近业务逻辑甚至人类思考层面的一种抽象,这个抽象层次其实已经比线程更高了。线程可让咱们的程序并发的跑,协程可让并发程序跑得看起来更美好。
线程自己就能够,为何要用协程呢?这就像咱们常常被人问起 Java 就能够解决问题,我为何要用 Kotlin 呢?为何你说呢?
总的来讲,无论是异步代码同步化,仍是并发代码简洁化,协程的出现实际上是为代码从计算机向人类思惟的贴近提供了可能。
欢迎关注 Kotlin 中文社区!
中文官网:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公众号:Kotlin
知乎专栏:Kotlin
CSDN:Kotlin中文社区
掘金:Kotlin中文社区
简书:Kotlin中文社区