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 的状态分为以下几种情况:

State isActive isCompleted isCancelled
New (optional initial state) False False False
Active(default initial state) True False False
Completing(transient state) True False False
Cancelling(transient state) False False True
Cancelled(final state) False True True
Completed(final state) False True False

那么一个 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 关键类分析

你可能感兴趣的:(kotlin)