摘要
协程更像是一种自动帮我们切换线程的工具,对于操作系统是透明的。此外,利用协程来写异步方法,也可以避免回调地狱。
正文
协程是轻量级线程(官方表述)
可以换个说法,协程就是方法调用封装成类线程的API。方法调用当然比线程切换轻量;而封装成类线程的API后,它形似线程(可手动启动、有各种运行状态、能够协作工作、能够并发执行)。因此从这个角度说,它是轻量级线程没错。
当然,协程绝不仅仅是方法调用,因为方法调用不能在一个方法执行到一半时挂起,之后又在原点恢复。这一点可以使用EventLoop之类的方式实现。想象一下在库级别将回调风格或Promise/Future风格的异步代码封装成同步风格,封装的结果就非常接近协程了。
而协程和线程之间的区别,往大了说,那就是普通函数与线程的区别;往小了说,就是EventLoop和线程的区别。他们之间的唯一的关系,仅仅在于协程的代码是运行在线程中。一个不恰当的类比,人和地球(地球提供生成环境,人在其中生存)
线程运行在内核态,协程运行在用户态
主要明白什么叫用户态,我们写的几乎所有代码,都执行在用户态,协程对于操作系统来说仅仅是第三方提供的库而已,当然运行在用户态。而线程是操作系统级别的东西,运行在内核态。
协程是一个线程框架(扔物线表述)
对某些语言,比如Kotlin,这样说是没有问题的,Kotlin的协程库可以指定协程运行的线程池,我们只需要操作协程,必要的线程切换操作交给库,从这个角度来说,协程就是一个线程框架。
但理论上我们可以在单线程语言如JavaScript、Python上实现协程(事实上他们已经实现了协程),这时我们再叫它线程框架可能就不合适了。
私以为,协程要从两方面看
概念上:coroutine(协程)和subroutine(子程序)是一个级别的(从命名上也类似)。子程序是一段具备一定功能的代码,一个函数、一个方法、一段代码都算是一个子程序。而协程,顾名思义,就是相互协作的子程序,多个子程序之间通过一定的机制相互关联、协作地完成某项任务。比如一个协程在执行上可以被分为多个子程序,每个子程序执行完成后主动挂起,等待合适的时机再恢复;一个协程被挂起时,线程可以执行其它子程序,从而达到线程高利用率的多任务处理目的——协程在一个线程上执行多个任务,而传统线程只能执行一个任务,从多任务执行的角度,协程自然比线程轻量。
通过提高线程利用率来提高多任务执行效率,这一点和IO多路复用、Reactor模型等基本思想一致,从这个角度看,协程并不是什么新东西。
实现上:协程的重点和难点就在于执行到挂起点时挂起和恢复的行为。它在底层技术实现上和我们常用的异步回调没有本质的区别,仅仅是根据不同的编程思想封装成对应的API。
其具体实现原理我们将在其它文章讨论,这里仅介绍协程概念。
协程解决的问题——以同步的方式写异步代码。如果不使用协程,我们目前能够使用的API形式主要有三种:纯回调风格(如AIO)、RxJava、Promise/Future风格,他们普遍存在回调地狱问题,解回调地狱只能通过行数换层数,且对于不熟悉异步风格的程序员来说,能够看懂较为复杂的异步代码就比较费劲。
调度器
调度器是协程上下文中众多元素中最重要的一个,通过CoroutineDispatcher定义,它控制了协程以何种策略分配到哪些线程上运行。这里介绍几种常见的调度器
Dispatcher.Default
默认调度器。它使用JVM的共享线程池,该调度器的最大并发度是CPU的核心数,默认为2
Dispatcher.Unconfined
非受限调度器,它不会将操作限制在任何线程上执行——在发起协程的线程上执行第一个挂起点之前的操作,在挂起点恢复后由对应的挂起函数决定接下来在哪个线程上执行。
Dispathcer.IO
IO调度器,他将阻塞的IO任务分流到一个共享的线程池中,使得不阻塞当前线程。该线程池大小为环境变量kotlinx.coroutines.io.parallelism的值,默认是64或核心数的较大者。
该调度器和Dispatchers.Default共享线程,因此使用withContext(Dispatchers.IO)创建新的协程不一定会导致线程的切换。
Dispathcer.Main
该调度器限制所有执行都在UI主线程,它是专门用于UI的,并且会随着平台的不同而不同
对于JS或Native,其效果等同于Dispatchers.Default
对于JVM,它是Android的主线程、JavaFx或者Swing EDT的dispatcher之一。
并且为了使用该调度器,还必须增加对应的组件
kotlinx-coroutines-android
kotlinx-coroutines-javafx
kotlinx-coroutines-swing
在其它支持协程的第三方库中,也存在对应的调度器,如Vertx的vertx.dispatcher(),它将协程分配到vertx的EventLoop线程池执行。
注意,由于上下文具有继承关系,因此启动子协程时不显式指定调度器时,子协程和父协程是使用相同调度器的。
使用
runBlocking{}
启动一个新协程,并阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成。
该方法的设计目的是让suspend风格编写的库能够在常规阻塞代码中使用,常在main方法和测试中使用。
GlobalScope.launch{}
在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。
由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。
实现CoroutineScope + launch{}
这是在应用中最推荐使用的协程使用方式——为自己的组件实现CoroutieScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。
例子
定义函数
/**
* 从服务器取信息
*/
private suspend fun getMessageFromNetwork(): String {
var name = ""
withContext(Dispatchers.IO) {
for (i in 0..1000000) {
//这里模拟一个耗时操作
}
name = "Huanglinqing1111"
}
return name
}
在Main函数:
GlobalScope.launch(Dispatchers.Main) {
var name = getMessageFromNetwork()
showMessage(name)
}
创建协程的方法有很多,有我们上面说的GlobalScope.launch方法,还有runBlocking方法
GlobalScope.launch 创建的是顶级协程,runBlocking创建的协程在协程作用域的代码没有执行完毕前会一直阻塞线程,所以上面。两个方法都不建议使用。
coroutineScope函数是一个挂起函数,它会继承外部的协程作用域并创建一个子协程,只能在协程作用域或者挂起函数中调用
launch函数必须在协程的作用域中才能调用。
说了这么多 在项目中我们改如何创建协程呢?
我们可以直接创建一个CoroutineScope对象,如下所示:
var coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch {
}
这样我们就创建了一个协程,可以按照上面的方法使用了
如果在页面打开的时候,我们在协程中进行网络请求,当页面销毁的时候我们也要将协程任务取消以免造成不必要的问题
网友评论