协程是什么?
协程的概念最核心的点其实就是函数或者一段程序能够被挂起(说暂停其实也没啥问题),待会儿再恢复,挂起和恢复是开发者的程序逻辑自己控制的,协程是通过主动挂起出让运行权来实现协作的(本质上是通过回调来实现的)。它跟线程最大的区别在于线程一旦开始执行,从任务的角度来看,就不会被暂停,直到任务结束这个过程都是连续的,线程之间是抢占式的调度,因此也不存在协作问题。
JVM
中的协程是只是一个线程框架吗?Kotlin
协程确实在实现的过程中提供了很方便切线程的能力,但这不是它的身份,只是它的一种能力而已。
协程相对于Handler,RxJava有什么优点
- 可以很方便的做到发起和取消任务
- 可以把运行在不同线程的代码放到一个代码块中。用看起来同步的方式写异步代码,本质上内部还是通过回调来实现的
协程的启动
可以通过launch() : Job
和async(): Deferred
来启动一个协程。Deferred
是Job
的子类,可以通过Job
对象进行取消Cancel
和Join
(挂起主协程,直到当前协程执行完成)操作。通过Deferred
可以得到协程执行结果,一般用于网络请求等操作
// GlobalScope 启动一个最外部全局协程 是一个单例对象
GlobalScope.lauch() {
val launch = launch {
// 启动二级协程
repeat(100) {
println("挂起中$it")
delay(500)
}
}
launch.join() // join方法,挂起当前主协程 直到launch协程执行完
delay(1600) // 挂起主协程 1600ms
launch.cancel() // cancel() 取消launch协程
// Deferred是Job的子类,多了await()获取执行结果
var job2: Deferred<String> = async {
delay(500);
return@async "hello"
}
println("async执行结果:" + job2.await()) //await为获取async协程执行结果
}
使用async并发
async
一般用于同时发起多个耗时请求,多个耗时请求的结果作用于同一个对象。打个比方,股票的报价和name通过两个接口获取,但是需求需要同时展示出来,使用async
就很方便处理
suspend fun getPrice(): Int {
delay(1000L) // 模拟接口耗时
return 198
}
suspend fun getName(): String {
delay(1000L) // 模拟接口耗时
return "AAPL"
}
//measureTimeMillis测量耗时
val time = measureTimeMillis {
val one:Deferred<Int> = async { getPrice() } // 已经在执行内容
val two:Deferred<String> = async { getName() } // 已经在执行内容
// await() 获取执行结果
println("name: ${two.await()} price: ${one.await()}")
// name: AAPL price: 198
}
println("Completed in $time ms") //Completed in 1017L ms
以上demo中,可以看到总耗时只有1000多点,在two.await()
虽然会阻塞当前协程,但是在await()
前,两个async
已经开始执行,two.await()
阻塞1000L后,执行one.await()
结果已经返回,不需要再等到1000L。
惰性启动的async
coroutine
一个可选的参数start
并传值CoroutineStart.LAZY
,可以对协程进行惰性操作。当一个start
函数被调用,协程才会被启动。
fun main() = runBlocking<Unit> {
val measureTimeMillis = measureTimeMillis {
val one: Deferred<Int> = async(start = CoroutineStart.LAZY) { doSomethingOne() }
val two: Deferred<Int> = async(start = CoroutineStart.LAZY) { doSomethingTwo() }
one.start()
two.start()
println("The value is ${one.await() + two.await()}")
}
println("TimeMillis $measureTimeMillis")
}
协程调度器CoroutineDispater
CoroutineDispater本身是协程上下文CoroutineContext的子类,同时实现了拦截器的接口。协程调度器的作用是将协程的执行局限在指定的线程
Dispatchers.Default // 在线程池中运行
Dispatchers.IO // 在线程池中运行,基于Default调度器背后的线程池,并实现了独立的队列和限制
Dispatchers.Main // 在主线程中运行,Android中为UI线程
Dispatchers.Unconfined // 在调用者线程直接启动
newSingleThreadContext("MyNewThread") // 新创建一个MyNewThread线程 在该线程中运行
通过 withContext
挂起协程并更改协程的执行线程,实现方便的线程切换。可以有返回值。
fun main() = runBlocking {
launch (Dispatchers.IO) {
// 模拟耗时操作
val text = getText()
withContext(Dispatchers.Main) {
// 模拟View展示内容
print(text)
}
}
launch (Dispatchers.Main) {
// 模拟耗时操作
val text = withContext(Dispatchers.IO){
getText()
}
// 模拟View展示内容
print(text)
}
}
协程的取消
通过Job
对象的cancel()
来取消协程,耗时挂起函数(比如delay) 需要加try catch保护 不然cancel
时则会抛出异常。协程中的轮询代码必须可配合isActive
取消
fun main(args: Array<String>) = runBlocking<Unit> {
val job = launch {
println("start")
try {
delay(1000L)
} catch (e:Exception) {
println("error")
}
println("1")
// isActive是协程内CoroutineScope对象的的一个扩展属性,判断协程是否可用,检查当前协程是否可以取消
while (isActive) {
// ...
}
if (isActive) {
println("2")
}
println("3")
delay(1000L)
println("end")
}
delay(100L)
job.cancelAndJoin() // 取消任务,并等待其结束
}
/// 打印
// start
// error
// 1
// 3
如果执行到 协程体内的代码需要依赖协程的cancel状态(比如delay方法),则会抛出异常。
如果协程体内的代码不依赖协程的cancel状态(比如println方法),则会继续往下执行
。也就是说协程的取消(cancel)
导致协程体终止运行的方式是抛出异常,如果协程体的代码不依赖协程的cancel
状态(即不会报错),则协程的取消 对协程体的执行一般没什么影响
作用域构建器coroutineScope
使用coroutineScope会构建一个新的协程作用域,其作用域属于上级协程,内部协程的取消和异常传播都是双向传播 ,coroutineScope
和runBlocking
很相似,他们都会挂起阻塞当前协程,但是coroutineScope
不会像runBlocking
那样阻塞线程。
fun main() = runBlocking<Unit> {
// coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
coroutineScope {
var job = launch {
delay(500L)
println("Task in coroutineScope")
}
delay(100L)
println("Task from coroutine scope")
}
println("主协程结束")
}
// Task from coroutine scope
// Task in coroutineScope
// 主协程结束
协程的几个作用域
- GlobalScope 启动的协程会单独启动一个作用域,其内部的子协程遵从默认的作用域规则,父协程向子协程单向传递,不能从子协程传向父协程
- coroutineScope 启动的协程会继承父协程的作用域,其内部的取消操作是双向传播的,协程未捕获的异常也会向上传递给父协程,也会向下传递给子协程。
- supervisorScope 启动的协程会继承父协程的作用域,他跟coroutineScope不一样的点是 它是由父协程向子协程单向传递的,即内部的取消操作和异常传递 只能由父协程向子协程传播,不能从子协程传向父协程 MainScope 就是使用的supervisorScope作用域,所以只需要子协程 出错 或 cancel 并不会影响父协程,从而也不会影响兄弟协程
协程异常传递模式
协程的异常传递跟协程作用域有关,要么跟coroutineScope一样双向传递,要么跟supervisorScope一样父协程向子协程单向传递
coroutineScope的双向传递
fun main() = runBlocking<Unit> {
// coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
try {
coroutineScope {
println("start")
launch {
// coroutineScope内部协程报错会传递给外面协程
throw NullPointerException("null")
}
delay(100L)
println("end")
}
} catch ( e: Exception) {
println("error")
}
}
// start
// error
fun main() = runBlocking<Unit> {
// coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
try {
coroutineScope {
println("start")
launch {
try {
delay(1000L)
println("launch")
} catch (e: Exception) {
println("inner error")
}
}
delay(100L)
throw NullPointerException("null")
println("end")
}
} catch ( e: Exception) {
println("out error")
}
}
// start
// inner error
// out error
coroutineScope作用域中的子协程 如果出现了异常,会传递给父协程。 父协程如果出现了传递也会传递给子协程,并且还会向外层协程传递。这种传递方式不便于管理,不建议使用
supervisorScope单向传递
fun main() = runBlocking<Unit> {
// coroutineScope会阻塞当前协程,直到其作用域内所有协程执行完成
try {
supervisorScope {
println("start")
launch {
throw NullPointerException("ddd")
}
delay(100L)
println("end")
}
} catch ( e:Exception ) {
println("error")
}
}
// start
// Exception in thread "main" java.lang.NullPointerException: ddd
// end
子协程的异常并不会向外层协程传递,并不会影响父协程的执行
挂起函数
suspend修饰的新函数,就是挂起函数。挂起函数只能在协程或者挂起函数中被调用,就像使用普通函数一样,但是它们有额外的特性——可以调用其他的挂起函数去挂起协程的执行。挂起的本质就是回调
delay 是一个特殊的 挂起函数 ,它不会造成线程阻塞,但是会挂起协程,并且只能在协程获取其他挂起函数中使用。
非阻塞式:用看起来阻塞的代码写出非阻塞的操作
fun main(args: Array<String>) = runBlocking<Unit> {
val job = launch { doWorld() }
println("Hello,")
job.join()
} // 这是你第一个挂起函数
suspend fun doWorld()
//比如在挂起函数doWorld中可以调用其他挂起函数delay,去挂起协程的执行
delay(1000L)
println("World!")
}
把回调切换成挂起函数
suspendCoroutine这个方法并不是帮我们启动协程的,它运行在协程当中并且帮我们获取到当前的协程Continuation实例,也就是拿到回调。方便后面我们调用它的 resume 来返回结果或者 resumeWithException 或者抛出异常。
通过 suspendCoroutine 可以实现把网络请求回调代码 转换成 挂起函数的方式
GlobalScope.launch(Dispatchers.Main) {
userNameView.text = getUserCoroutine().name
}
suspend fun getUserCoroutine() = suspendCoroutine<User> { continuation ->
getUser(object : Callback<User> {
override fun onSuccess(value: User) {
continuation.resume(value)
}
override fun onError(t: Throwable) {
continuation.resumeWithException(t)
}
})
}
fun getUser(callback: Callback<User>) {
val call = OkHttpClient().newCall(
Request.Builder()
.get().url("https://api.github.com/users/bennyhuo")
.build())
call.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
callback.onError(e)
}
override fun onResponse(call: Call, response: Response) {
response.body()?.let {
try {
callback.onSuccess(User.from(it.string()))
} catch (e: Exception) {
callback.onError(e) // 这里可能是解析异常
}
}?: callback.onError(NullPointerException("ResponseBody is null."))
}
})
}
如果想通过取消协程来实现取消网络请求,那么就需要使用suspendCancellableCoroutine可以设置一个协程取消的回调,在回调中取消网络请求
suspend fun getUserCoroutine() = suspendCancellableCoroutine<User> { continuation ->
val call = OkHttpClient().newCall(
Request.Builder()
.get().url("https://api.github.com/users/bennyhuo")
.build())
continuation.invokeOnCancellation {
log("invokeOnCancellation: cancel the request.")
call.cancel()
}
call.enqueue(object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call, response: Response) {
response.body()?.let {
try {
continuation.resume(User.from(it.string()))
} catch (e: Exception) {
continuation.resumeWithException(e) // 这里可能是解析异常
}
} ?: continuation.resumeWithException(NullPointerException("ResponseBody is null."))
}
})
}
协程的通信Channel
Channel主要用于协程之间的通信。Channel是一个和阻塞队列BlockingQueue非常相似的概念。
fun main(args: Array<String>) = runBlocking<Unit> {
val channel = Channel<Int>()
launch {
put(channel)
}
launch {
get(channel)
}
}
suspend fun put(channel: Channel<Int>) {
var i = 0
while (true) {
channel.send(i++)
}
}
suspend fun get(channel: Channel<Int>) {
while (true) {
println(channel.receive())
}
}
Channel可以通过close关闭来表示没有更多的元素进入通道。使用for循环从通道中接收元素,如果不使用close指令,那么接收协程会一直被挂起
//创建缓冲个数为5的Channel通道
val channel = Channel<Int>(5)
launch {
for (x in 1..5) channel.send(x * x)
channel.close() // 我们结束发送
}
// 这里我们使用 `for` 循环来打印所有被接收到的元素(直到通道被关闭)
for (y in channel)
println(y)
println("Done!")
produce函数
一般不如上直接使用Channel,而是使用 produce协程函数。Channel拥有缓存对象,可以设置缓存个数。当Channel中缓存满时,再调用send函数时会将当前协程挂起。
fun main(args: Array<String>) = runBlocking<Unit> {
val product:ReceiveChannel<Int> = put()
get(product).join()
delay(2000L)
product.cancel()
}
//produce协程函数返回得到一个Channel capacity = 5声明Channel缓存个数为5
fun CoroutineScope.put() = produce(capacity = 5, context = CommonPool) {
var i = 0
while (isActive) {
//channel的值等于缓存个数的时候 调用send方法会被挂起
send(i++)
}
}
fun CoroutineScope.get(channel: ReceiveChannel<Int>) = launch {
channel.take(10).consumeEach {
println("接受到$it")
}
}
Android开发中使用协程
首先要实现 CoroutineScope
这个接口,CoroutineScope
的实现教由代理实例 MainScope()
MainScope()
的返回实例是CoroutineScope
的子类,声明了context
为Dispatchers.Main+SupervisorJob
。就是 SupervisorJob
整合了 Dispatchers.Main
而已,它的异常传播是自上而下的,这一点与 supervisorScope
的行为一致,此外,作用域内的调度是基于 Android
主线程的调度器的,因此作用域内除非明确声明调度器,协程体都调度在主线程执行。
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//加载并显示数据
loadDataAndShow()
}
private fun loadDataAndShow(){
launch(Dispatchers.IO) {
//IO 线程里拉取数据
var result = fetchData()
withContext(Dispatchers.Main){
//主线程里更新 UI
text.setText(result)
}
}
}
suspend fun fetchData():String{
delay(2000)
return "content"
}
override fun onDestroy() {
super.onDestroy()
// 触发作用域协程的取消,那么该作用域内的协程将不再继续执行:
cancel()
}
}
retrofit2对协程的支持
retrofit2 2.6.0版本以上才开始支持协程
data class Repository(val id: Int,
val name: String,
val html_url: String)
interface SplashInterface {
// 正常的Call返回
@GET("/repos/{owner}/{repo}")
fun contributors(@Path("owner") owner: String,
@Path("repo") repo: String): Call<Repository>
// 协程的Deferred
@GET("/repos/{owner}/{repo}")
fun contributors2(@Path("owner") owner: String,
@Path("repo") repo: String): Deferred<Repository>
// 协程的suspend
@GET("/repos/{owner}/{repo}")
suspend fun contributors3(@Path("owner") owner: String,
@Path("repo") repo: String): Repository
}
public class SplashModel {
suspend fun sendNetworkRequestSuspend() = RetrofitHelper.getInstance().createService(SplashInterface::class.java).
contributors3("square","retrofit")
suspend fun sendNetworkRequest() = RetrofitHelper.getInstance().createService(SplashInterface::class.java).
contributors2("square","retrofit").await()
}
fun getData() {
launch(Dispatchers.Main) {
try {
getPageView()?.setData(model.sendNetworkRequest())
} catch (e: Exception) {
e.printStackTrace()
}
}
}