kotlin之协程 kotlin之协程的理解与使用详解
何33512336 人气:0前言
为什么在kotlin要使用协程呢,这好比去了重庆不吃火锅一样的道理。协程的概念并不陌生,在python也有提及。任何事务的作用大多是对于所依赖的环境相应而生的,协程对于kotlin这门语言也不例外。协程的优点,总的来说有如下几点:轻量级,占用更少的系统资源; 更高的执行效率; 挂起函数较于实现Runnable或Callable接口更加方便可控; kotlin.coroutine 核心库的支持,让编写异步代码更加简单。当然在一些不适应它的用法下以上优势也会成为劣势。
1.协程定义
协程定义:kotlin官方基于JVM的线程实现的一个并发任务处理框架,封装的线程api
- 使用方便,不使用回调实现线程切换,使用同步方式写出异步代码
- 所有的耗时任务保证一定放在后台执行挂起
- 函数执行完毕之后,协程会把它切换到原先的线程的线程。
2.协程的基本用法
常规函数中一般都有:call and return,协程在此之外添加了suspend和resume.
- suspend 用于暂停执行的当前协程,并保存所有的局部变量
- resume 用于已暂停的协程中暂停出恢复
supend(挂起函数)是什么,有什么意义
suspend,对协程的挂起并没有实际作用,其实只是一个提醒,函数创建者对函数的调用者的提醒,提醒调用者我是需要耗时操作,需要用挂起的方式,在协程中使用.
- 需要注意的是挂起函数只能在挂起函数或者协程作用域中使用,为什么挂起函数需要在协程作用域中使用?因为普通函数没有suspend和resume这两个特性,所以必须要在协程的作用中使用。
- 意义:
- 语法层面:作为一个标记和提醒。通过报错来提醒调用者和编译器,这是一个耗时函数,需要放在后台执行。
- 编译器层面:辅助 Kotlin 编译器来把代码转换成 JVM 的字节码。
- 怎么自定义suspend函数?
- 什么时候定义?
- 需要耗时操作的时候,需要定义,例如io耗时操作(请求网络);获取数据库数据;一些等待一会需要的操作;列表排除,json解析等;
- 怎么写suspend函数,给函数前加上suspend 关键字,把内容用withContext包起来
suspend fun testSuspendfun(){ withContext(Dispatchers.IO){ } }
协程如何确保主线程安全
- Dispatchers.Main 调用程序在Android的主线程中
- Dispatchers.IO 适合主线程之外的执行磁盘或者网络io操作,例如文件的读取与写入,任何的网络请求
- Dispatcher.Default 适合主线程之外的,cpu的操作,例如json数据的解析,以及列表的排序,
协程的挂起本质:本质就是切线程,完成之后只不过可以自动切回来
协程挂起就是切个线程,在挂起函数执行完毕之后,协程会自动的重新切回它原先的线程,也就是稍后会被切回来的线程切换。切回来就是resume,恢复功能是协程,所以suspend函数需要在另一个suspend函数或者协程中调用。「非阻塞式挂起」阻塞的方式写出了非阻塞的方式。
3.协程的创建以及取消
//创建一个协程 Val scope = CoroutineScope(Dispatchers.Main+Job()) 通过Job获取协程的生命周期 scope.launch{ } 其他耗时请求,例如从数据库中获取数据 scope.async { }
在KTX库为某些生命周期提供自己的CoroutineScope,例如ViewModel中viewModelScope,Lifecycle有lifecycleScope
协程的启动,launch 启动新协程而不将结果返回给调用方
//创建之后,不管后续 launch(){ }
async 启动一个新协程,并通过deferred的await方法暂停函数
//返回deferred 对象 val deferred async{ } deferred.await()
协程的结构化并发,取消协程
协程的结构化并发,可以让协程非常便于管理。例如在关闭activity中要取消协程。如果是在线程中,取消所有的线程比较复杂。
取消父协程以及父里面的子协程
val scope = CoroutineScope(Dispatchers.Main+ Job()) scope.launch { val job = launch { val job1 = launch { } } job.cancel() } scope.cancel()
取消子协程某一个,每一个协程都会返回一个job对象,通过调用job的cancle,可以去取消单个的协程的。
val scope = CoroutineScope(Dispatchers.Main+ Job()) scope.launch { val job = launch { val job1 = launch { } } job.cancel() } scope.cancel()
4.协程中异常处理
在协程内部中捕获异常
val scope = CoroutineScope(Dispatchers.Main+ Job()) scope.launch { try { }catch (e:Exception){ } }
5.协程的优势
在程序运行过程中某些操作(像是:网络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() } }
内存使用情况:
运行耗时:10分钟以上。
使用线程的代码,占用的内存几乎是使用协程的两倍。而且从运行时间上看使用协程实现的程序话费的时间要远远低于线程的实现方式。单从这两点来看,协程拥有更高的执行效率,占用更少的系统资源。那么Kotlin中的协程是通过什么来实现异步操作的呢?它使用的是一种叫做 挂起 的机制。协程的挂起几乎是没有损耗的,换种说法,就是不需要选择额外的上下文或是操作系统调用。 另外一点, 挂起能很大程度上被用户库给控制:我们可以决定在挂起状态下具体做些什么,并且围绕着需求进行优化/日志/拦截等操作。
协程不能随随便便就被挂起,只能在一个称为挂起点的地方,在这里会去调用特别标记的函数。这样的函数被称作 挂起函数,因为你调用他们会挂起一个协程(如果允许这次调用的话,库可以直接进行处理而不需要挂起)。 挂起函数的声明需要添加suspend修饰符。例如:
suspend fun doSomething(foo: Foo): Bar { ... }
挂起函数就像平常使用的函数一样,可以有参数和返回值,但是他们只能被协程或是其它挂起函数调用。事实上,要想启动一个协程,至少得有一个挂起函数,并且一般是匿名的(也就是一个挂起lambda表达式)。
线程往往是没有返回值(实现Runnable接口),尽管可以通过实现Callable接口来获得带返回值的线程。但这与协程在语法层面上的支持,在使用的便捷性上还是有不少差距的。
协程是通过编译技术实现的 (不需要虚拟机或操作系统的特别支持),这一点在开头也提到了。挂起操作通过代码变换实现。基本上,每一个挂起函数(可能会进行优化,但我们在着不想讨论这点)都被转换成一个状态机,那些状态与挂起调用相对应。在一个挂起准备好之前,下一状态与相关局部变量等一起存储在编译器生成的类的字段中。在恢复该协程时,恢复局部变量并且状态机从刚好挂起之后的状态进行。挂起的协程可以作为保持其挂起状态与局部变量的对象来存储和传递。
许多其它语言实现的异步机制也能制作成库,在Kotlin的协程中使用。包括:C#和ECMAScript写的 async/await , channels Go语言写的 select ;C#和Python写的 generators/yield 。
加载全部内容