协程在Kotlin中文文档的解释是轻量级的线程,Go、Python 等很多现成语言在语言层面上都实现协程,不过Kotlin和他们不同的的是,Kotlin协程本质上只是一套基于原生Java线程池 的封装,Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。
private fun test3(){
Log.e("test", "start")
lifecycleScope.launch {
launch()
}
Log.e("test", "end")
}
suspend fun launch(){
Log.e("test", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
运行结果如下:
image.png
从截图可以看出,launch函数被挂起,然后主要流程继续执行,而launch函数被挂起后也继续执行。
suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", Thread.currentThread().name)
delay(3000)
Log.e("test", "launch_end")
}
}
在lifecycleScope里,就更简单了,直接如下
private fun test3() {
Log.e("test", "start")
lifecycleScope.launch(Dispatchers.IO) {
Log.e("test", "launch_start")
delay(3000)
withContext(Dispatchers.Main){
Log.e("test", "launch_end")
}
}
Log.e("test", "end")
}
在协程里,线程切换就是这么简单,在io线程执行耗时任务,然后又在main里切会主线程。
我们开启了一个协程,在协程执行期间也想操作这个协程,这就是要用到Job,什么是Job,从概念上讲,一个 Job 表示具有生命周期的、可以取消的东西。从形态上将,Job 是一个接口,但是它有具体的合约和状态,所以它可以被当做一个抽象类来看待。
Job 一共包含六个状态:
Job 的生命周期会经过四个状态:New → Active → Completing → Completed。
image.png
下面来讲讲job的常用方法:
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.join()
Log.e("test", "end")
}
private suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
}
运行结果如下:
image.png
可以看到,加上join,协程代码会在这里执行,并且是阻塞的,只有执行完才会走下一步
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.cancelAndJoin()
Log.e("test", "end")
}
运行结果如下:
image.png
可以看待,cancelAndJoin(),会运行,然后取消,取消完后会走下一步
代码如下:
private suspend fun test3() {
Log.e("test", "start")
job=lifecycleScope.launch(start = CoroutineStart.LAZY) {
launch()
}
Log.e("test", "end")
job?.start()
}
运行效果如下:
image.png
可以看到当设置延迟加载时,协程是start()后才开始执行
说到延迟加载,在总结一下协程启动模式
对于不同协程构造器,异常的处理方式不同。分别介绍 launch 和 async 情况下的异常处理
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
}finally{
//结束处理
}
}
也可以try catch整个协程
try {
scope.launch {
codeThatCanThrowExceptions()
}
} catch(e: Exception) {
// Handle exception
}finally{
//结束处理
}
}
fun main() = runBlocking {
val deferred = GlobalScope.async {
throw Exception()
}
try {
deferred.await() //抛出异常
} catch (t: Throwable) {
println("捕获异常:$t")
}finally{
//结束处理
}
}
到目前为止,上面的代码都是串行的,即从上到下依次执行,而协程不单单串行,我们也可以并行的方式。
private fun test() {
Log.e("test", "start")
lifecycleScope.async {
Log.e("test", "launch1_start")
delay(1000)
Log.e("test", "launch1_end")
}
lifecycleScope.async {
Log.e("test", "launch2_start")
delay(2000)
Log.e("test", "launch2_end")
}
Log.e("test", "end")
}
运行效果如下
image.png
可以看到两个协程可以并行执行,也可以用await()方法,代码如下:
private fun test4(){
lifecycleScope.launch {
val a=async {
delay(1000)
1
}
val b=async {
delay(5000)
2
}
var c=a.await()+b.await()
Log.e("test",c.toString())
}
}
通过await()方法,即使两个协程完成时间不一致,最终也可以一起运算。
从上面可以了解到,协程也是可以并发的,既然是并发,那同样也会出现像java多线程并发的问题,导致各种问题,协程本身也提供了两种方式处理并发:
使用mutex.withLock {*} 即可实现数据的同步以简化使用,下面给个事例: 在没有加锁之前:
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
value++
Log.e("test",value.toString())
}
}
}
开启五个协程,同时运行,对value操作:
image.png
可以看到,顺序是乱的,而加了mutex之后呢:
var mutex= Mutex()
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
mutex.withLock {
value++
Log.e("test",value.toString())
}
}
}
}
运行结果如下:
image.png
可以看到,加锁之后,都会等上一个运行后之后在解锁,在运行下一个。
一个 actor 是由协程、被限制并封装到该协程中的状态以及一个与其它协程通信的 通道 组合而成的一个实体。一个简单的actor 可以简单的写成一个函数,但是一个拥有复杂状态的actor更适合由类来表示。
有一个 actor 协程构建器,它可以方便地将 actor 的通道组合到其作用域中(用来接收消息)、组合发送 channel 与结果集对象,这样对actor的单个引用就可以作为其句柄持有。
使用 actor 的第一步是定义一个 actor 要处理的消息类。
// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求
接下来定义一个函数,使用 actor 协程构建器来启动一个 actor:
// 这个函数启动一个新的计数器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor 状态
for (msg in channel) { // 即将到来消息的迭代器
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}
执行代码:
runBlocking {
val counterActor = counterActor() // 创建该 actor
repeat(100) {
launch {
repeat(1000) {
counterActor.send(IncCounter)
}
}
}
delay(3000)
// 发送一条消息以用来从一个 actor 中获取计数值
val response = CompletableDeferred<Int>()
counterActor.send(GetCounter(response))
println("Counter = ${response.await()}")
counterActor.close() // 关闭该actor
}
actor 可以修改自己的私有状态,但只能通过消息互相影响(避免任何锁定)。actor 在高负载下比锁更有效,因为在这种情况下它总是有工作要做,而且根本不需要切换到不同的上下文,这样效率更高。
写到这里,基本上把协程的基本用法都说了,最后要用协程,要知道这么创建协程吧,其实这里也有分的,所以才放在最后,假如是单单在kotlin里创建协程,就有三种方式
runBlocking {
...
}
通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
GlobalScope.launch {
...
}
GlobalScope和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
...
}
这是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context 和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。
首先需要引用ktx库
implementation "androidx.lifecycle:lifecycle-runtime-ktx:版本号"
这个时候我们就可以在activity或者framgent直接使用lifecycleScope进行启动协程。就像我上面的代码实例一样。
lifecycleScope和lifecycle的生命周期一致,退出的时候也可以自动取消协程,不用自己手动取消。
同时,还扩展了lifecycleScope.launchWhenResumed , lifecycleScope.launchWhenCreated ,lifecycleScope.launchWhenStarted, 分别对应activity或者fragment的onResumed(),onCreated(),onStarted().
同样引入扩展库
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:版本号"
引入库之后,我们就可以在ViewModel用viewModelScope来使用协程.
其他情况下的创建按照上面协程推荐的第三种方式即可