图片来自必应

本文是对官方文档中协程的教程的翻译加上个人理解,也可以直接阅读官方文档:Your first coroutine with Kotlin

协程可以认为是一个轻量级的线程,和线程一样,它可以同时运行、等待运行或者马上运行。它与线程最大的不同在于协程的开销非常低,几乎不需要开销。我们可以创建数千个协程,并且只付出很少的性能损耗。从另一方面来说,真正的线程去开启并且运行它是十分昂贵的,数千个线程对现代机器的性能来说是个十分严峻的挑战。

  • 引入协程

引入协程的方法很简单,只需要在app的build.gradle 文件中引入coroutine支持:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
}
复制代码

那么我们如何开始使用协程? 让我们来看看 lunch{}函数:

GlobalScope.lunch{
    ...
}
复制代码

上述代码将会开启一个新的协程, GlobalScope 表示该协程的生命周期仅受整个应用的生命周期影响。当然我们也可以创建一个新的协程基于某一个线程的生命周期,例如:

CoroutineScope(newSingleThreadContext("thread-1")).launch {  }
复制代码

在默认情况下,协程会运行在线程共享池。在基于协程的程序中线程仍然会存在,但是一个线程能够运行很多协程,所以我们并不需要创建很多的线程, 我们来看使用协程的一整段代码:

fun main(args: Array<String>){
    println("Start")
    
    GlobalScope.launch{
        delay(1000)
        println("Hello")
    }
    
    Thread.sleep(2000)
    println("Stop")
}
复制代码

从上面的代码可以看出来,我们可以使用delay() 函数类似Thread() 类中的 sleep() 方法,但是这个方法的好处在于:它并不会像Thread().sleep() 那样会阻塞线程,而仅仅是暂停当前的协程。当协程暂停的时候,当前的线程将会释放,当协程暂停结束的时候,它将会在线程池中的空闲的线程上恢复,这样就意味着,如果我们使用协程,我们就可以不用像线程那样去使用回调处理返回结果,虽然RxJava可以做到等待结果返回,但是也没有协程这样方便简洁

如果在主线程的话,必须要等待我们协程完成,否则上面的例子将会在“Hello”打印之前结束了。

让我们将上面的Thread().sleep(2000) 这句代码注释掉,那么结果将会是先打印“Stop”,再打印“Hello”。

如果我们直接使用同样的非阻塞方法 delay() 在主线程内,将会出现编译错误:

Suspend functions are only allowed to be called from a coroutine or another suspend function

这个错误是因为我们使用了delay( ) 而没有在任何的协程中,我们可以通过runBlocking{} 来启动一个协程并且阻塞直到其完成:

runBlocking{
    delay(1000)
}
复制代码

现在,对于上面的例子来说,首先会打印“Start” ,然后会运行到launch{},然后会运行runBlocking{} 直到它完成,然后打印“Stop”,与此同时第一个协程完成并且打印“Hello”。

  • async: 返回协程的值

还有一种开启一个协程的方法为async{}, 在这方面它和launch{} 具有一样的效果,但是async{}会返回一个Deferred<T> 的实例,这个实例有一个方法await(),这个方法可以返回协程的结果。

让我们再来看一段代码,先来运行一百万个协程,并且将它们返回的Deferred对象保存起来。然后计算结果:

val deferred = (1..1_000_000).map{
    n -> async{
        n
    }
}
复制代码

当所有的都启动了之后,我们显然需要收集它们的结果:

val sum = deferred.sumBy{it.await()}
复制代码

上面的代码看上去好像没有什么问题,我们把每个协程的结果拿到之后对其求和,看上去好像一切正常,但是编译器却报错了:

Suspend functions are only allowed to be called from a coroutine or another suspend function
复制代码

显然,await() 不能够被使用在协程之外,因为await() 会暂停协程知道它完成,然而只有协程能够被不阻塞的暂停,所以,我们应该将await() 写在协程里面:

runBlocking{
    val sum = deferred.sumBy{it.await()}
    println("Sum: $sum")
}
复制代码
  • 挂起函数(Suspending functions)

正如文章开头提到的,协程最大的优点就是可以不通过阻塞而挂起线程,编译器必须要通过一些特殊的代码而去实现这个功能,所以我们必须要显式的说明那些可能会挂起(suspend)的代码,所以可以使用suspend去说明:

suspend fun workload(n: Int): Int{
    delay(1000)
    return n
}
复制代码

当我们使用suspend显式的说明workload() 函数可能会suspend之后,当我们从协程中调用它,编译器就会知道这个函数将会suspend并且做好相应的准备:

async{
    workload(n)
}
复制代码

这时workload(n) 将能够从协程或者其他的suspend函数中调用,但是不能够在协程以外调用。相应的,delay()await() 是被默认声明为suspend的, 这就是为什么必须要在runBlocking{}launch{}async{} 中才能够调用它们的原因。

以上就是kotlin协程中一些基本概念与使用,关于协程的更多用法会在之后的文章中再一一说明。