Java崽学习Kotlin---协程快速入门
ZealSinger 发布于 阅读:60 Kotlin
协程
虽然说kotlin支持协程是它的一大特点,但是实际上Kotlin 仅在标准库中提供最基本底层 API 以便其他库能够利用协程。与许多其他具有类似功能的语言不同,async
与 await
在 Kotlin 中并不是关键字,甚至都不是标准库的一部分,其实也可以理解为kotlin只是在最底层提供了API能利用协程,但是本质上不是kotlin自己支持的
所以使用协程,首先就得需要导入对应的依赖,主流的就是kotlinx-coroutines-core,是Jetbains封装的协程库
---Maven----
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.10.2</version>
</dependency>
---Gradle---
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
基本概念
-
协程
大伙儿应该知道线程是轻量级进程,而协程可以理解为轻量级线程或者说微线程,其特点是一种用户级别的线程,拥有自己的寄存器上下文和栈
线程和协程最被大家所熟知的功能应该就是多任务处理,但是线程和协程作为并发设施上存在区别,线程是抢占式多任务,而协程是协作式多任务 线程/进程在切换和阻塞的时候都会让CPU陷入系统调用,会需要CPU跑操作系统的调度程序然后由调度程序决定跑哪个线程/进程,也就是因为这个决定策略,是一种抢占式调度,也就是执行顺序是不可控的,所以如果需要可控和处理同步问题,需要小心翼翼的处理
但是协程不会出现这个问题,协程是自己编写调度逻辑,因为在CPU看来协程就是一个单线程,CPU不用去考虑怎么调度,切换上下文,省去了CPU切换开销,同时我们必须显示的产出才能让程序的剩余部分运行,不会像线程一样随时可能被中断,中断变少了也就无需保留锁,协程自身会进行同步操作,因为在任何时刻都只有一个协程运行
-
suspend function
挂起函数,delay是协程库提供的挂起函数,挂起函数使用关键字suspend修饰,挂起函数不会阻塞所在线程,而是将协程挂起,在特定的时候再恢复执行
-
CoroutineScope
协程作用域,所有的协程都需要运行在某个协程作用域内,这样也方便我们通过作用域控制管理协程,例如协程的生命周期
-
CoroutineContext
协程上下文
-
CoroutineBuilder
协程构造器
协程的实现方式有点类似于线程,下面是一个段协程的案例代码,在如下代码中,如果使用线程的方式,其实就是将GlobalScope.launch{....} 换成new Thread(......).start,并且将delay替换为Thread.sleep();两者的效果是一致的,下面代码能看得懂个大概就行,我们后面详细解释,大致体验一下kotlin中的协程
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后台启动一个新的协程并继续
delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
println("World!") // 在延迟后打印输出
}
println("Hello,") // 协程已在等待时主线程还在继续
Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}
创建作用域
按照上面基本概念中说的,每个协程首先必须得运行在一个协程作用域中,在Kotlin的协程库中,有很多的构造器方法可以用于创建协程作用域,但是基本上都不是直接创建的协程作用域对象的
GlobalScope
如上案例代码中,我们的协程代码就是在GlobalScope.launch{......} 结构中,所以很明显,GlobalScope也是一个可以创建作用域的函数,其会创建一个在整个Kotlin中都可以直接使用的协程作用域,显然,这个范围很大,不方便我们进行细致化控制和管理协程,所以实际上用的比较少
当我们使用的时候,Idea都会给我们黄色的警告
需要注意的是,GlobalaScope是创建的作用域,而不是协程的创建,真正创建协程的函数是后面的 launch()以及另外一个 async()函数(即GlobalScope.async{......}也可以),具体区别和讲解在下面
coroutineScope函数(最常用)
最常用的创建协程作用域的方式是coroutineScope函数,其作用就是创建一个CoroutineScope,执行放在其内部的协程并且等待内部所有协程完毕之后再退出/返回
我们将上述案例进行改造,我们将打印World的逻辑提取出来作为一个函数,因为这个函数肯定是需要协程异步执行的,所以这里需要使用suspend修饰
然后这里也可以看到,线程中使用Thread.sleep进行阻塞休眠,协程中使用delay()进行阻塞休眠
fun main() {
GlobalScope.launch {
doWorld()
}
println("Hello,") // 先打印这个 协程此时还在delay(2000L)等待中
Thread.sleep(3000L) // 主线程中主动休眠保证在协程完成之前不会结束进程,确保协程执行完毕
}
// suspend修饰
suspend fun doWorld(){
coroutineScope {
launch {
delay(2000L)
println("World!")
}
}
}
runBlocking函数
从上面代码中我们可以看到,主线程的Thread.sleep()好像是必须的,但是其实,我们还有一个runBlocking函数来创建作用域,其特点是:会阻塞线程,保证调用线程一定比其内部的协程晚点运行和退出,可以知道,这个作用一般作用域主线程中
fun main() {
runBlocking{
launch {
doWorld()
}
println("Hello,") // 因为deworld还在delay 先打印这个
} // 阻塞main线程的执行 保证进程不会结束
}
suspend fun doWorld(){
coroutineScope {
launch {
delay(2000L)
println("World!")
}
}
}
框架中的CoroutineScope
大多数情况下我们自己不需要创建协程作用域,而是框架会为我们准备好,就例如Jetpack中的ViewModel,其作用就是将UI操作的逻辑封装起来,那么所有的ViewModel中的协程操作都会运行在viewModeScope环境内
运行上下文
协程可以理解为是一个可以异步执行的函数,launch{....}创建协程并且执行{.....}中的协程逻辑,协程代码块执行完毕之后就会立马去执行{}外面的逻辑
协程代码块的执行是在写成里面的,什么时候返回是未知的,也可以让协程挂起,协程挂起之后就会释放运行他的线程,并不会阻塞运行他的线程,也就让别的协程有运行的机会
例如上述GlobalScope作用域中调用doWorld协程异步方法,doWorld()方法就是挂起状态,不会锁主main线程,这也是为啥 println("Hello")会被先执行和打印,main线程没有被阻塞而是会继续进行
协程我们说了可以理解为一个函数,那么函数肯定执行在一个线程环境中或者理解为被在某个地方被调用执行,这个协程运行的线程环境一般就是协程运行上下文,一般而言,除非指定切换线程执行,协程一般就是在主线程中被运行
协程的运行环境由CoroutineContext来定义,但是也同样,我们一般不会直接创建这个对象,而是通过参数或者其他构建函数来指定协程的运行上下文环境
通过参数指定上下文环境
创建协程的方法有launch和async,这两个方法可以有多个参数,其中都包含了指定运行上下文的相关参数,其方法的完整参数如下
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
第一个参数就是指定运行上下文,那么可以写出如下案例代码,指定协程的运行上下文/线程环境
import kotlinx.coroutines.*
fun main() {
runBlocking{
launch {
doWorld()
}
println("Hello,")
}
}
suspend fun doWorld(){
coroutineScope {
// 指定协程环境
/*
Dispatchers是Kotlin中为了方便决定协程的运行上下文/线程环境定义的调度器类
主要包括了Dispatchers.Default , Dispatchers.io
Dispatchers.Main Dispatchers.Unconfined四种已经定义好的方案,当然也可以自定义调度器
四个默认调度器的底层其实就是Executors即线程池,不同的策略底层的线程池的相关参数配置也不同,适用于不同的应用场景
Dispatchers.Default,底层是一个核心线程数等于CPU核心数的线程池,适合CPU密集行的任务
Dispatchers.IO 底层是一个弹性线程池,可以按照需求进行拓展核心线程数,适合IO密集型任务
Dispatchers.Main 即调用主线程作为线程环境,一般用于更新UI/接交互
Dispatchers.Unconfied 不限制协程的线程环境,随着调用线程启动,岁恢复线程继续执行,容易导致协程在不同的线程中执行,慎用
*/
launch(Dispatchers.Default){
delay(2000L)
println("World!")
}
}
}
拓展函数withContext
除了上述的在launth 和 async函数的参数中指定运行环境,还可以使用拓展函数withContext指定上下文环境,其接受两个参数,第一个参数也是CoroutineContext上下文环境,第二个参数就是协程函数的lambda表达式,该函数就会用第一个参数环境去执行第二个参数协程逻辑
其函数原型如下
suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T{....}
将上述案例修改,使用withContext指定运行上下文环境
fun main() {
runBlocking{
launch {
doWorld()
}
println("Hello,")
}
}
suspend fun doWorld(){
coroutineScope {
// 下面两种效果是一模一样的 只是两种写法
// 按照函数原型标准调用方式
launch{
withContext(Dispatchers.Default,{
delay(2000L)
println("World!")
})
// 可以看之前的基础语法中的函数部分的入参相关设计中有说到,最后一个参数为lambda的时候在调用的时候可以将lambda逻辑写到()外边
withContext(Dispatchers.Default){
delay(2000L)
println("World!")
}
}
}
}
withContext通常用于一些需要对外不透明的运行上下文的设置,对外提供suspend方法允许被调用,但是suspend方法中自己有定义环境而不需要受限于或者说去关心调用者/用户的运行环境
如下 UserRepository对外提供login方法,它只向外部暴露一些suspend方法,在这些suspend方法内部通过withContext来指定它自己运行的上下文环境,从而不用管调用者的执行环境,不也需要调用者知道repo的执行环境
class UserRepository(
val dispatcher: Dispatcher = Dispathers.IO
) {
suspend fun login() {
withContext(dispatcher) {
// Do login
}
}
}
框架自带的运行上下文环境
虽然可以自己制定运行参数,如果全部自己定义也就意味着更多的管理和很多的context,事实上,现在很多并发框架都有对应的自己的配置好的运行上下文环境,不需要我们会过多考虑或者说不需要我们很细致的操作
协程可控性(Job对象和Deferred对象)
我们知道,Java中对于一个异步任务,我们想要知道其是否完成或者说获取其完成后的结果,是通过Futrue或者COmplateableFutrue实现的,可以通过Futrue判断线程是否完成和获取结果
对于协程,Kotlin存在类似的用于控制协程的机制,主要是依靠的Job对象和Deferred对象,launch创建的协程返回的是Job对象,async创建的协程返回的是Deferred对象,同时这也是之前没说的一个点,launch创建协程和async创建协程的区别
fun main() {
runBlocking{
launch {
doWorld()
}
}
}
suspend fun doWorld(){
coroutineScope {
val job:Job = launch{
delay(3000L)
println("Coroutines#1")
}
val deferred = async{
delay(1000L)
println("Coroutines#2")
}
println("主线程同时正在执行别的任务.....")
// 注意两者对应的方法名是不一样的 job是join() deferred是await()
job.join()
deferred.await()
println("All coroutines have finished.")
}
}
/* 输出如下
主线程同时正在执行别的任务.....
Coroutines#2
Coroutines#1
All coroutines have finished.
*/
Job对象和Deferred对象之间除了和协程的创建方式不同之外,还有如下的区别
-
Job等待携程完成的方法是 join()方法 而Deferred等待完成的方法是 await()方法
-
Deferred其实是Job对象的一个子类,其拥有的拓展功能是可以绒布等待完成并且获取运行结果,而Job的join方法只能阻塞等待完成而不能获取到结果
fun main() { runBlocking{ val job = launch(CoroutineName("job")) { // 这里是类似Java中的new Thread(threadName) 一样 给协程命名 方便我们在输出中进行查看 doWorld(100) } val deferred= async(CoroutineName("deferred")) { doWorld(10) } // 在输出可以看出 job.join() 只会阻塞等待而不会获取协程返回值 其函数原型中本来就是没有返回值 println("result = ${job.join()} + ${deferred.await()}") } } suspend fun doWorld(time:Int):Int{ return coroutineScope { delay(1000L+time) // 直接利用 coroutineContext运行上下文获取协程名 println("coroutine ${coroutineContext[CoroutineName]?.name} finished") return@coroutineScope time-1 } } /* 输出如下 coroutine deferred finished coroutine job finished result = kotlin.Unit + 9 */
需要注意的是,kotlin中Job的join方法和deferred的await方法,所谓的同步等待,和Java中的Thread的sleep,Futrue的get,join方法这些的阻塞等待是不一样的
Kotlin中的同步等待是对于主线程而言是非阻塞的,而Java中多线程就是真正的阻塞等待,Kotlin中同步等待只是把协程进行挂起,将运行环境上下文中的线程环境给释放,允许其它的任务使用CPU资源即线程,虽然也是要等待协程任务完成之后才会执行后面的任务,但是在等待的这个期间是main主线程是可以被使用的(只是类似我们如上的代码,那个时候main线程没有其他任务需要进行了,所以可以看起来和Thread的get和join没区别),但是在Thread中,get和join是真正的阻塞线程,会导致main线程不能被其他任务使用
文章标题:Java崽学习Kotlin---协程快速入门
文章链接:https://zealsinger.xyz/?post=3
本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自ZealSinger !
如果觉得文章对您有用,请随意打赏。
您的支持是我们继续创作的动力!

微信扫一扫

支付宝扫一扫