Job.cancel()无法取消协程的问题及Retrofit/Room调用时的取消疑问
我先还原下你的测试场景哈,你写了个Repository类,用delay(3000)模拟API请求的延迟,然后尝试用cancel()方法取消协程:
class Repository { var job: Job? = null fun getAll() { job = CoroutineScope(Dispatchers.IO).launch { try { delay(3000) println("getAll oke") } catch (e: Exception) { println("CancellationException") } } } fun cancel() { job?.cancel() println("cancel") } } fun main() { runBlocking { val api = Repository() launch { api.getAll() } delay(1000) api.cancel() delay(5000) println("END") } }
这段代码的输出是:
cancel CancellationException END
但如果把delay(3000)换成Thread.sleep(3000),输出就变成了:
cancel getAll oke END
你已经摸透了核心原因:delay() 是可取消的挂起函数,它会主动检查协程的取消状态,一旦检测到协程被取消,就会抛出CancellationException终止协程;而Thread.sleep() 是阻塞线程的普通方法,完全感知不到协程的存在,自然不会响应取消请求,所以哪怕调用了cancel(),协程里的代码还是会跑完。
接下来重点解答你的疑问:如果getAll()里是Retrofit或者Room的调用,调用cancel()能取消这些操作吗?
答案是:可以的,只要用官方适配协程的挂起函数方式调用。
对于Retrofit:从2.6.0版本开始,官方原生支持挂起函数接口,比如你定义的API接口是这样的:
interface ApiService { @GET("data") suspend fun fetchData(): Response<Data> }这种挂起函数版本的请求是支持协程取消的。当你调用
job.cancel()时,Retrofit会自动取消底层的OkHttp请求,协程也会随之终止,不会继续执行后续代码。对于Room:Room同样支持挂起函数,比如Dao里的查询方法定义成挂起函数:
@Dao interface DataDao { @Query("SELECT * FROM data") suspend fun getAllData(): List<Data> }这种挂起函数的数据库操作也是可取消的,当协程被取消时,Room会终止当前的查询操作,协程也会停止执行。
它们能被取消的原因是:Retrofit和Room的挂起函数内部都实现了可取消的逻辑,会在合适的时机检查协程的取消状态,就像delay()一样,一旦检测到取消就抛出异常终止协程,同时终止底层的网络请求或数据库操作。
不过要注意:如果用的是Retrofit的Call异步回调方式(非挂起函数),或者Room的非挂起函数+RxJava/Callback的方式,那协程的cancel()就无法直接取消这些操作了,必须手动管理请求的取消(比如调用Call的cancel方法)。
备注:内容来源于stack exchange,提问作者Nguyễn Mạnh Cường

