使用Kotlin协程的崩溃纪实(二)
前言
由于笔者对Kotlin协程缺乏深刻的理解以及充分的实践,导致在实际工作中使用Kotlin协程时遇到一些崩溃的问题。
那么本文主要记录遇到的这些崩溃问题,这是其中之二。
背景
在Kotlin协程中如果使用下标访问集合数据,那么可能会抛出IndexOutOfBoundsException
异常导致崩溃。
本文记录的崩溃情况与第一篇中的属于同一种类型,只是触发的代码逻辑和业务逻辑略有不同,所以本文的风格与第一篇雷同。
伪代码
下面是符合背景中描述的伪代码:
1 | fun main() { |
上面的伪代码大致的逻辑如下:
-
创建一个可变列表
ml
并添加**[0, 10]**这11条数据, -
创建一个
RunSuspend
的实例run
,用于主线程等待协程执行完毕,否则主线程退出,协程无法执行完毕, -
创建一个协程作用域
scope,
启动一个协程A,- 在协程中首先获取
Element(10)
的下标(indexOf
), - 然后调用
suspendWork
挂起函数模拟执行耗时任务, suspendWork
挂起函数执行完毕后,再通过第一步获取的下标(indexOf
)获取对应的Element
,- 协程体执行完毕后或抛出异常时调用
run.resumeWith
通知主线程协程执行完毕并停止阻塞主线程,
- 在协程中首先获取
-
再创建一个
RunSuspend
的实例await
,用于阻塞主线程500毫秒,然后启动一个新的协程B移除ml
可变列表中的Element(10)
, -
调用
run.await
阻塞主线程并等待协程A执行完毕。
伪代码执行的期望结果如下:
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
笔者期望伪代码可以正常的执行完毕。
但是,实际上伪代码执行的结果如下:
1 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
执行结果分析:
- 协程A中仅输出了第一步获取到
Element(10)
下标(indexOf
)的日志, await
实例阻塞主线程500毫秒后,我们在启动的协程B中移除ml
可变列表中的Element(10)
,并输出日志移除成功:true
,- 回到协程A,等到协程体中的
suspendWork
挂起函数执行完毕后,想通过下标(indexOf
)获取对应的Element
时,抛出异常,
分析
为什么执行结果会与我们期望的结果不同呢?接下来我们分析下原因。
首先我们再仔细观察下协程A中的代码:
1 | scope.launch { |
按照笔者的想法,协程A中协程体的执行逻辑如下:
- 协程体是在同一个线程中执行,
- 协程体是同步且顺序执行的,
可以简单理解协程体的执行是线程安全的。
真的线程安全么?
针对第一点,目前的scope
没有指定运行的线程,那么协程默认是在Default
线程池中执行,同时循环体的每次执行都会触发两次线程切换:
- 其一是从
launch
协程体执行的线程切换至suspendWork
挂起函数执行的线程, - 其二是
suspendWork
挂起函数执行完毕后,从suspendWork
挂断函数执行线程切换回launch
协程体执行的线程,
而launch
协程体执行的线程由Default
线程池分配,线程池分配线程存在不固定性,所以协程体在同一个线程中执行不能成立,自然不能称为是线程安全的。
为什么会有这样的想法呢?
正如前言中所说,笔者对协程缺乏深刻的理解以及充分的实践,而协程的一大特点是:使用「同步代码」写出异步程序。
其实笔者就是被「同步代码」这一表象所“欺骗”,这也是笔者对协程缺乏深刻理解的佐证。
使用「同步代码」写出异步程序,对程序猿来说这是多么美好的事情,但是如果对协程理解的不够深入,不清楚它背后的逻辑,那么很容易就像笔者一样被它简单的表象所“欺骗”。
针对第二点,协程的特点是使用「同步代码」写出异步程序,在协程体中调用了挂起函数,那么协程体中的逻辑必然是异步程序,所以第二点也不成立。
异常原因
本文记录的崩溃原因比较好分析:协程A第一步中获取的下标(idnexOf
),在经过协程B移除Element(10)
之后就变成了一个过期的下标(indexOf
),等到协程A中suspendWork
挂起函数执行完毕,想通过过期的下标(indexOf
)获取对应的Element
时,必然会抛出IndexOutOfBoundsException
异常。
如何修复?
修复方案有多种,比如:
- 在协程A第一步获取到下标(
indexOf
)之后,suspendWork
挂起函数之前,通过第一步获取到的下标(indexOf
)获取对应的Element
, - 在协程A中的
suspendWork
挂起函数执行完毕后,需判断下标(indexOf
)是否过期, - 使用加锁的方式,协程A操作
ml
集合时不允许其他协程对ml
集合进行更新, - 其他方式,
具体选择哪种修复方案,这里可以根据业务场景的不同而选择不同的修复方案。
总结
本文记录了笔者在使用Kotlin协程过程中遇到的下标过期导致的崩溃问题,通过伪代码笔者复现了崩溃问题,并分析了问题产生的原因以及给出一些修复方案供选择参考。
本文与第一篇中记录的情况,笔者都是被协程特点:使用「同步代码」写出异步程序所"欺骗",所以在协程体中调用其他的挂起函数时,挂起函数的前后代码最好不要有关联性,最好保持独立性,否则可能会发生意想不到的情况,归根结底还是笔者对协程缺乏深刻的理解以及充分的实践。
其中最重要的是发现自身的不足,发现自身的不足也是一种进步。
纸上得来终觉浅,绝知此事要躬行。