Bootstrap

kotlin coroutines 协程教程(一)基本用法

kotlin coroutines 协程

Coroutine 协程,是kotlin 上的一个轻量级的线程库,对比 java 的 Executor,主要有以下特点:

  1. 更轻量级的 api 实现协程
  2. async 和 await 不作为标准库的一部分
  3. suspend 函数,也就是挂起函数是比 java future 和 promise 更安全并且更容易使用

那么实际本质上和线程池有什么区别呢?我的理解是这样的,协程是在用户态对线程进行管理的,不同于线程池,协程进一步管理了不同协程切换的上下文,协程间的通信,协程挂起,对于线程挂起,粒度更小,而且一般不会直接占用到CPU 资源,所以在编程发展的过程中,广义上可以认为 多进程->多线程->协程。

简单使用

首先,要引入 coroutines 的依赖,在你的 build.gradle

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0'
}

然后下面是一个最简单的例子,在子线程延迟打印一行日志:

fun coroTest() {
    // Globals 是 Coroutines 的一个 builder
    GlobalScope.launch {
        delay(1000L)//Delays coroutine for a given time without blocking a thread and resumes it after a specified time.
        Thread.sleep(2000)
        Log.i(CO_TAG, "launch ")
    }
    Log.i(CO_TAG, "----")
}

控制台输出效果如下:

01-05 11:11:40.373 28131-28131/com.yy.yylite.kotlinshare I/coroutine: ----
01-05 11:11:43.375 28131-3159/com.yy.yylite.kotlinshare I/coroutine: launch 

也就是说在子线程 delay 1000 毫秒,然后 sleep 2000 毫秒,之后打印出来了,确实达到了我们理想的状态。

接着看下 Android studio 的 cpu profiler,我们可以看到,这里启动了几个新的子线程:

这里会创建名为DefaultDispatch 的子线程,做个一个简单的实验,不断的重复执行上面的代码,并不会无限创建子线程,看了其内部的线程数也是有约束的。这个可能比直接调度线程池,更加节省资源,也避免了极端情况。(实际上 delay() 是一个非阻塞的挂起函数)

blocking 和 non-blocking 函数

delay{} 是 非阻塞函数,Thread.sleep() 则是阻塞函数,coroutines 中使用 runBlocking{} 作为阻塞函数,例如以下代码:

    fun testBlockAndNoBlock() {
        //非阻塞,子线程
        GlobalScope.launch {
            delay(1000)
            doLog("no-block")
        }
        doLog("non block test")
        //会阻塞主线程
        runBlocking {
            delay(3000)
            doLog("block")
        }
        doLog("block test")
    }

则控制台输出结果为:

01-05 15:27:37.982 20264-20264/com.yy.yylite.kotlinshare I/coroutine: non block test
01-05 15:27:38.984 20264-20312/com.yy.yylite.kotlinshare I/coroutine: no-block
01-05 15:27:40.983 20264-20264/com.yy.yylite.kotlinshare I/coroutine: block
    block test

结果就是使用 runBlocking 会阻塞主线程,那么这个在实际开发中有任何用途吗?实际上,runBlocking{} 不是直接用在协程中的,常常用于桥接一些挂起函数操作,用于顶底函数或者Junit Test中,例如如下代码:

fun testBlock() = runBlocking {
   val job= launch { 
        delay(1000)
       doLog("in run block")
    }
    job.join()
}

这里将 join() 和 launch{} 进行桥接,使他们能够在一个地方执行。

等待

上面提到了可以通过 delay() 来等待一个函数执行,并且是非阻塞的,coroutines 中也提供了另一种等待机制,简单的例子如下:

fun testWaitJob() {
    val job = GlobalScope.launch {
        delay(2000)
        doLog("waite")
    }
    doLog("main doing")
    GlobalScope.launch {
        job.join()
        doLog("really excute")
    }
}

最终输出结果如下,也就是 join()方法

01-05 21:45:53.472 13230-13230/com.yy.yylite.kotlinshare I/coroutine: main doing
01-05 21:45:55.475 13230-13803/com.yy.yylite.kotlinshare I/coroutine: waite
01-05 21:45:55.476 13230-13803/com.yy.yylite.kotlinshare I/coroutine: really excute
任务取消

某个场景下,你开启了一个协程,但是因为一些原因,你要取消这个协程,那么你可以这样处理,使用一下 Job.cancel() 方法取消协程,如下例子:

fun testCancel2() {
    doLog("test cancel")
    val job = GlobalScope.launch {
        for (index in 1..30) {
            doLog("print $index")
            delay(100)
        }
    }
    doLog("no waite repeat")
    GlobalScope.launch {
        delay(1000)
        doLog("cancel ")
        job.cancel()
    }
}

控制台输出如下,也就是表示通过 job.cancel() 将执行的协程取消了。

01-05 21:52:20.684 17521-17521/com.yy.yylite.kotlinshare I/coroutine: test cancel
01-05 21:52:20.696 17521-17521/com.yy.yylite.kotlinshare I/coroutine: no waite repeat
01-05 21:52:20.698 17521-17784/com.yy.yylite.kotlinshare I/coroutine: print 1
01-05 21:52:20.801 17521-17788/com.yy.yylite.kotlinshare I/coroutine: print 2
01-05 21:52:20.901 17521-17785/com.yy.yylite.kotlinshare I/coroutine: print 3
01-05 21:52:21.002 17521-17787/com.yy.yylite.kotlinshare I/coroutine: print 4
01-05 21:52:21.105 17521-17787/com.yy.yylite.kotlinshare I/coroutine: print 5
01-05 21:52:21.219 17521-17793/com.yy.yylite.kotlinshare I/coroutine: print 6
01-05 21:52:21.320 17521-17785/com.yy.yylite.kotlinshare I/coroutine: print 7
01-05 21:52:21.421 17521-17788/com.yy.yylite.kotlinshare I/coroutine: print 8
01-05 21:52:21.522 17521-17786/com.yy.yylite.kotlinshare I/coroutine: print 9
01-05 21:52:21.623 17521-17793/com.yy.yylite.kotlinshare I/coroutine: print 10
01-05 21:52:21.706 17521-17799/com.yy.yylite.kotlinshare I/coroutine: cancel 

但是实际上, Job 的状态分为以下几种情况:

StateisActiveisCompletedisCancelled
New (optional initial state)FalseFalseFalse
Active(default initial state)TrueFalseFalse
Completing(transient state)TrueFalseFalse
Cancelling(transient state)FalseFalseTrue
Cancelled(final state)FalseTrueTrue
Completed(final state)FalseTrueFalse

那么一个 job 的执行流程如下:

* +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
* | New | -----> | Active | ---------> | Completing  | -------> | Completed |
* +-----+        +--------+            +-------------+          +-----------+
*                  |  cancel / fail       |
*                  |     +----------------+
*                  |     |
*                  V     V
*              +------------+                           finish  +-----------+
*              | Cancelling | --------------------------------> | Cancelled |
*              +------------+                                   +-----------+

那么一个job 的状态根据执行过程,不断发生变化。其次,子job 和父job 相互关联,取消父job 会先依次取消子 job,同样子 Job 取消或者失败也会影响到父 Job 。

launch{} , runBlocking{} ,async{}

launch{} 会在当前线程开启一个新的协程,并且不会阻塞当前线程,同时会返回一个 Job 做为 coroutine 的引用,你可以通过这个 Job 取消对应的 Coroutine。

runBlocking {} 会在开启一个新的协程,并且阻塞当前进程,直到操作完成。这个函数不应该在协程里面使用,它是用来桥接需要阻塞的挂起函数,主要用于 main function 和 junit 测试。也就是说,runBolcking {} 必须用在最上层。

async{} 会在对应的 CoroutineContext 下创建一个新的协程,并且放回一个Deferred,通过 Deferred 可以异步获取结果,也就是调用Deffered 的 await() 方法。

先来看下三者的源码:

//launch
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

//runBlocking
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}
//async
public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

在 launch 里面会创建一个新的 CoroutineContext,如果没有传入 Context 则使用的 EmptyCoroutineContext,通过 newCoroutineContext() 函数会分配一个默认的 Dispatcher,也就是 Dispatcher.default,默认的全局 Dispatcher,会在jvm 层级共享线程池,会创建等于cpu 内核数目的线程(但是至少创建两个子线程)。接着判断 CoroutineStart 是否 Lazy 模式,如果 Lazy 模式,则该 Coroutine 不会立马执行,需要你主动掉了 Job.start() 之后才会执行。

如果想要了解 Coroutine 原理,请查看下一篇文章 Coroutine 关键类分析

;