Kotlin Coroutine(协程)简介

1,110 阅读4分钟

Kotlin Coroutine(协程)系列:
1. Kotlin Coroutine(协程) 简介
2. Kotlin Coroutine(协程) 基本知识

协程介绍

协程是可挂起计算的实例。

它在概念上类似于线程,在这个意义上,它需要一个代码块运行,并具有类似的生命周期,它可以被创建和启动,但它不绑定到任何特定的线程。

它可以在一个线程中挂起其执行,并在另一个线程中恢复。而且,像future 或 promise那样,它在完结时可能伴随着某种结果(值或异常)

协程开发人员这样描述协程:

协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

如上面所说,协程是由开发者自己控制的,因此在使用协程时我们一定要记住一点,我们必须知道我们使用的协程在何时挂起,它又在何时重新恢复执行,如果没法知道这两点,那就意味着我们无法控制协程,这个时候要慎用协程。

为什么使用协程

使用协程可以提高线程的利用率。

通常我们在Android中发起一个网络请求都会经历如下几步:

  1. 在主线程中创建一个请求任务,如:Retrofit.Call
  2. 为这个任务分配一个子线程去执行请求任务,如:调用Retrofit.Call.enqueue(callback)方法
  3. 子线程发起请求后将会阻塞等待网络请求的返回结果,拿到结果后会将数据转换成我们需要的实体对象
  4. 在主线程中执行回调接口,执行余下的业务操作

上面的流程中为请求任务分配子线程一般都会配合线程池去做,以防止不断创建线程而产生系统开销,但在线程真正执行过程中经常会遇到因磁盘IO或者是网络请求等操作而导致线程阻塞,而此时当前线程只能阻塞等待,无法做任何事情,在等待的这段时间里线程相当于白白了浪费了自身资源,导致线程自身利用率低下。

Android中改用协程发起网络请求流程如下:

  1. 在主线程中创建一个协程,在协程中创建网络请求任务
  2. 为协程分配一个子线程去发起网络请求
  3. 挂起子线程中的协程,此时仅仅是协程挂起,该子线程并没有挂起阻塞
  4. 协程等待请求结果回来之后,会在子线程中重新恢复协程执行
  5. 在主线程中执行某个回调,拿到请求数据执行余下的业务操作

在上述流程步骤3中挂起协程后子线程并不会阻塞,此时该子线程可以被系统分配去做其他的事情,当协程挂起结束时重新在子线程中恢复执行。这样该线程就不会存在因阻塞导致的空闲浪费,提高了线程利用率。

总的来说,使用协程可以最大程度的复用线程,通过让线程满载运行,从而达到充分的利用CPU提高系统性能。

告别回调地狱

使用协程另外一个好处就是可以让开发者们告别异步编程中的回调地狱,简化异步编程,让写异步代码和写同步代码一样简单,增强了代码的可读性、可理解性和可维护性。

举个例子
假定有个登录有如下流程:

  1. 发请求获取用户token
  2. 根据token获取用户信息

常用实现方式代码如下:

fun login() {
    requestToken { token ->
        requestUserInfo(token) { user ->
            Log.i("tag", user.toString())
        }
    }
}

上面的例子是Android开发中经常会遇到的问题,一个请求依赖前一个请求的结果,这个时候经常会出现这样的写法,在第一个请求的成功回调中根据请求结果发起第二个网络请求。这里还只存在两层的嵌套,试想一下,如果嵌套层次出现4次,5次,甚至更多会出现怎样的情况,估计开发者自己写起来都会崩溃。

使用RxJava实现代码如下:

Single.fromCallable { requestToken() }
    .map { token -> requestUserInfo(tokenm) }
    .subscribe(
        { user -> Log.i("tag", user.toString()) }, // onSuccess
        { e -> e.printStackTrace() }  // onError
    )

使用RxJava的实现方式虽然将回调嵌套改成了链式写法,阅读起来要稍微好点,但是依然存在回调而且增加了实现的复杂度,对不熟悉RxJava的人来说更少增加了难度。

使用协程实现方式代码如下:

fun login() {
    val token = requestToken()
    val user = requestUserInfo(token)
    Log.i("tag", user.toString())
}

怎么样,使用协程的写法是不是简便很多,而且看起来非常符合人们的阅读和理解习惯。