Kotlin中的协程,用起来原来这么简单?

导航

  • Kotlin中的协程,用起来原来这么简单?
    • 协程和线程
      • 线程:
      • 协程
    • 协程的使用
      • 准备工作
      • 开始使用
        • 启动一个协程
        • runBlocking,阻塞线程的协程
        • launch,不阻塞线程的协程
        • async,异步取值原来可以这么简单
        • withContext 你叫我去哪,我就去哪
        • suspend(Cancellable)Coroutine,你站这别动,我好了会叫你的
      • 使用总结
    • 总结

Kotlin中的协程,用起来原来这么简单?

协程和线程

线程:

在Java中,线程大家应该都很熟悉了,我就不做过多的解释了。java中的线程实现是和操作系统中的线程是一对一的关系,所以在java中线程的创建和销毁是很重量级的操作,当然使用线程池是一种很好的优化方案。

协程

众所周知,线程可以说是轻量级的进程,那么协程就可以说是轻量级的线程。在Java中线程受操作系统调度,jvm对线程没有太多控制权,而协程的控制权是掌握在程序本身的,是可以用代码控制的。不仅如此,协程的创建和销毁是非常轻量级的操作,有多轻量级呢?在Kotlin中,协程就是一个对象实例,这下明白了吧,创建协程就像创建普通对象一样简单。

协程的使用

准备工作

首先你得要有一定的Kotlin语言基础,不懂的地方可以去Kotlin中文官网学习

要在Kotlin中使用协程,首先在项目中导入Kotlin协程核心库,kotlinx-coroutines-core,Kotlin协程核心库是使用Kotlin协程必须导入的,当然还可以根据项目的不同再导入其他扩展库。这里仅需要导入协程核心库即可。

开始使用

启动一个协程

启动一个协程的方法可多了去了,Kotlin提供了很多启动协程的扩展方法,这里就讲讲常用的几种,直接上代码:

fun main() = runBlocking {
    println("runBlocking协程")
    GlobalScope.launch {
        println("GlobalScope.launch协程")
    }.join() // 这里的join现在不理解没关系
    GlobalScope.async {
        println("GlobalScope.async协程")
    }.await() // 这里的await现在不理解没关系
}

执行结果如下:

runBlocking协程
GlobalScope.launch协程
GlobalScope.async协程

我们重点看runBlocking,GlobalScope.launch,GlobalScope.async这三个方法。这三个方法中任一方法执行都会创建并启动一个协程。


首先看runBlocking的方法定义:

public fun <T> runBlocking(...): T

方法参数暂时不用管,所以这里省略了,runBlocking方法是一个泛型方法,并且是Kotlin中的顶层方法,在任何地方调用runBlocking方法后都会创建一个运行在当前线程的协程,直到runBlocking方法返回后才会运行后面的代码逻辑。由于runBlocking的这个特性,runBlocking的用途很有限。


再来看一下GlobalScope.launch方法定义:

public fun CoroutineScope.launch(...): Job

同样省略了方法参数,CoroutineScope.launch也是一个Kotlin中的顶层方法,啥,方法名错了?说好的GlobalScope.launch呢?并没有错,GlobalScope.launch调用的就是CoroutineScope的扩展方法launch,不信你看GlobalScope类定义:

public object GlobalScope : CoroutineScope

GlobalScope继承自CoroutineScope, 所以GlobalScope.launch调用的就是CoroutineScope.launch方法,从方法定义可以看到,它返回的是一个Job对象,这里先不用深究Job对象有什么用,你暂且只要知道它可以用来启动或者取消这个协程就行了。


接下来看GlobalScope.async方法定义:

public fun <T> CoroutineScope.async(...): Deferred<T>

按照惯例,省略方法参数,聪明的小伙伴一定猜到了,GlobalScope.async调用的就是CoroutineScope.async方法。没错,就是这么回事。CoroutineScope.async同样是Kotlin的顶层方法。也是一个泛型方法,从方法定义可知它返回一个Deferred对象,那这个Deferred对象是干嘛的呢,Deferred翻译过来就是延期的意思,这个对象有一个重要的方法就是await方法,这个方法被调用后,如果结果已经可用,那么会直接返回结果,如果结果不可用,那么会暂停调用方协程,直到await结果可用返回或者有异常从中抛出。


上面说了这么多次顶层方法,那么为什么要定义成顶层方法呢?因为在Kotlin中顶层方法是可以在任何地方都能调用的,当然前提是权限允许。这样就可以在任何地方创建协程了。以上就是在Kotlin中启动一个协程的方法,当然启动协程的方法b不只这些,这里只是介绍了一部分而已。

runBlocking,阻塞线程的协程

调用runBlocking方法后会创建并启动一个协程,并且只有待到runBlocking中的代码逻辑全部执行完成后,runBlocking后面的代码逻辑才会得到执行,来,看个例子:

fun main() {
    println("我在runBlocking之前 ${System.currentTimeMillis()}")
    runBlocking {
        println("我在runBlocking之中 ${System.currentTimeMillis()}")
        delay(2000) // 在协程中调用该方法会暂停当前协程指定时间,并不会阻塞线程
    }
    println("我在runBlocking之后 ${System.currentTimeMillis()}")
}

执行结果:

我在runBlocking之前 1588400909039
我在runBlocking之中 1588400909040
我在runBlocking之后 1588400911043

从结果不难看出runBlocking之后的代码是在runBlocking结束之后才得到执行的,所以可以得出结论: runBlocking会阻塞当前线程直到协程中的任务执行完成才会返回继续执行runBlocking后面的代码逻辑。那么这样的阻塞式协程在何时会用到呢?这个问题其实很简单,runBlocking其实可以和main方法无缝衔接使用,以下写法可以说是runBlocking的典型用法了:

fun main() = runBlocking {
    // 代码逻辑
}

像上面这种写法,在学习Kotlin协程时是很有用的,就是因为它会阻塞当前线程的这一特性。现在可能有人就会问: 既然有阻塞线程的协程,那应该有不阻塞线程的协程吧。答案是肯定的,其实绝大多数协程都是不会阻塞线程的,线程中的阻塞可以和协程中的暂停对应,但不能混作一谈,在协程中多个协程是依靠暂停来相互协作的。下面就来看看不阻塞线程的协程。

launch,不阻塞线程的协程

刚才我们讲了阻塞线程的协程,现在就讲讲不阻塞线程的协程。直接调用GlobalScope.launch就会创建并启动一个协程,并且该方法会直接返回。先看一段代码直观体验一下:

fun main() {
    println("main方法开始")
    GlobalScope.launch {
        println("GlobalScope.launch启动")
        delay(1000) // 防止协程过快结束
        println("GlobalScope.launch结束")
    }
    println("main方法结束")
}

执行结果:

main方法开始
main方法结束
GlobalScope.launch启动

从执行结果来看,可以发现虽然协程的创建是在println("main方法开始")println("main方法结束")之间,但是协程真正运行是在println("main方法结束")之后,同时由于main方法结束,协程也就随之结束,只打印出了“GlobalScope.launch启动”,而没有后续了。像这样的协程就是不阻塞线程的协程。

假如你现在有这么一个需求,你现在需要执行一段非常耗时的操作,而当前线程有无法等待这么久,就像Android中的UI线程那样,像这样的问题你会怎么处理呢?答案很简单,相信你也很快就想到解决办法了,把耗时操作丢到子线程中去处理就行了呀,对就是这么简单粗暴。但有了协程之后,我们就可以这些写:

fun main() = runBlocking {
    println("runBlocking开始")
    launch {
        var count = 0L
        repeat(Int.MAX_VALUE) {
            count++
        }
        println("耗时任务结束 count = $count")
    }
    println("runBlocking结束")
}

执行结果:

runBlocking开始
runBlocking结束
耗时任务结束 count = 2147483647

从运行结果可以看出,launch中的运算是发生在println("runBlocking结束")之后的,也就是异步的,并且成功打印了运算结果。注意,这里是在runBlocking中是直接调用launch,而没有加GlobalScope.,这会儿一定会有人问: 为什么没家GlobalScope.呢?加和不加又有什么区别呢?别急,这就给你解释清楚。

这里有一件是是很重要的,那就是协程与协程之间是存在父子关系的,父协程一定在其子协程结束之后才会结束。也就是说,父协程在执行完自己的代码逻辑后会检测与自己关联的子协程的状态,如果还有子协程没有执行完(这里的没执行完是指协程处于活动状态,也就是isActive返回true的情况。如果isActive返回false则算是执行完了,即使还有代码逻辑在运行。),会一直等到子协程结束才会结束自己。

正因为协程中存在父子关系,上面我在runBlocking中才会直接调用launch方法创建协程,而不是调用GlobalScope.launch来创建协程。假如将上面直接调用launch改成GlobalScope.launch,仅仅改成像下面这样的:

fun main() = runBlocking {
    println("runBlocking开始")
    GlobalScope.launch {
        var count = 0L
        repeat(Int.MAX_VALUE) {
            count++
        }
        println("耗时任务结束 count = $count")
    }
    println("runBlocking结束")
}

执行结果:

runBlocking开始
runBlocking结束

看,并没有打印出即宋人结果。为什么会是这样呢?如果你理解了上文对协程父子关系的介绍,相信你已经知道原因了。没错,就是因为通过调用GlobalScope.launch方法创建的协程是没有父协程的,而在runBlocking中直接调用launch方法创建的协程是会建立父子关系,这样runBlocking会等到launch创建的协程执行完才结束。

这时,又有小伙伴要问了,为什么可以在runBlocking中直接调用launch方法呢?这个问题得从runBlocking方法的定义说起,这回就要把参数带上了:

public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

第一个参数暂时先不用管,重点在第二个参数。第二个参数的类型是定义在CoroutineScope下的返回值为T的suspend的扩展方法。也就是一个CoroutineScope的扩展方法,既然是CoroutineScope的扩展方法,那么在这个方法中自然就可以调用CoroutineScope对象中的所有的public的(扩展)方法或(扩展)字段。

而launch方法是定义在CoroutineScope下的public修饰的方法,自然是可以被调用的。这下明白为什么runBlocking中为什么可以直接调用launch方法了吧。

这会儿又有人要问了,如果我非要用GlobalScope.launch可以吗?可以,当然可以,但不是所有情况下都可以,只有在suspend修饰的方法中才可以,而从上面runBlocking的方法定义中可以看出传入的第二个参数就是一个suspend修饰的扩展方法,所以可以用GlobalScope.launch替换launch,像下面这样:

fun main() = runBlocking {
    println("runBlocking开始")
    val job = GlobalScope.launch {
        var count = 0L
        repeat(Int.MAX_VALUE) {
            count++
        }
        println("耗时任务结束 count = $count")
    }
    println("runBlocking结束")
    job.join() // 让当前协程等待job结束
}

执行结果:

runBlocking开始
runBlocking结束
耗时任务结束 count = 2147483647

可以看到,这回我们定义了一个变量job用来接受GlobalScope返回的结果,那么这个job是什么类型的呢?相信认真的你一定记得。没错就是Job类型的,上文我只是说了Job可以用来控制协程的开始和取消,但Job的用处并不只这些,它还可以用来查看协程的状态,可以用来协调多个协程的合作。上述中调用job.join()方法就是为了让runBlocking等待job结束。先来看一下join方法的定义:

public interface Job : CoroutineContext.Element {
    ...
    public suspend fun join()
    ...
}

可以看到join方法是用suspend修饰的,在Kotlin中,suspend方法只能在suspend方法中被调用,这是在kotlin中的语法要求,知道就行了。由于join方法是在suspend方法,所以只有在suspend方法中才能被调用。而要等待协程执行完成需要依靠join方法。这就是为什么我说不是所有情况下都可以将launch换成GlobalScope.launch的原因。

launch就先介绍到这里,下面将教你如何用单线程思维写并发任务。

async,异步取值原来可以这么简单

上文介绍了async方法的定义,调用async方法会创建并启动一个协程,并且返回一个Deferred对象实例,那么这个async在什么时候使用呢?先别急,我们先假设有下面这段代码:

fun main() = runBlocking {
    val timeMillis = measureTimeMillis {
        val first = {
            Thread.sleep(1500) // 模拟耗时任务
            10
        }.invoke()
        val second = {
            Thread.sleep(2000) // 模拟耗时任务
            10
        }.invoke()
        println("first + second = ${first + second}")
    }
    println("timeMillis = ${timeMillis}")
}

执行结果为:

first + second = 30
timeMillis = 3501

这是一个很典型的串行任务,代码从上到下依次执行,现在就用async改写它:

fun main() = runBlocking {
    val timeMillis = measureTimeMillis {
        val first = GlobalScope.async {
            Thread.sleep(1500) // 模拟耗时任务
            10
        }.await()
        val second =  GlobalScope.async {
            Thread.sleep(2000) // 模拟耗时任务
            10
        }.await()
        println("first + second = ${first + second}")
    }
    println("timeMillis = ${timeMillis}")
}

执行结果:

first + second = 30
timeMillis = 3521

噫,怎么改用async后更耗时了呢?先别急,我再做一些小调整:

fun main() = runBlocking {
    val timeMillis = measureTimeMillis {
        val first = GlobalScope.async {
            Thread.sleep(1500) // 模拟耗时任务
            10
        }
        val second =  GlobalScope.async {
            Thread.sleep(2000) // 模拟耗时任务
            10
        }
        println("first + second = ${first.await() + second.await()}")
    }
    println("timeMillis = ${timeMillis}")
}

执行结果:

first + second = 30
timeMillis = 2023

这回没问题了,first和second的计算是并行的了,看,写并行任务是不是就和写串行任务一样简单?这是就有人要问了,同样是使用GlobalScope.async为什么上面那个从结果来看还是串行的,而下面这个就成并行了呢?要理解这个问题,首先找出二者的不同点: 上面那个是在GlobalScope.async调用之后直接调用了await(), 而下面则是在两个GlobalScope.async方法调用返回之后调用的await(),这就是原因。

文章开始开头介绍了async的返回的是一个Deferred对象实例,并且也说明了await()的一些特性,不记得了的话,这里再讲一遍: await方法被调用后,如果结果已经可用,那么会直接返回结果,如果结果不可用,那么会暂停调用方协程,直到await结果可用返回或者有异常从中抛出。这下知道了吧,调用await时如果结果不可用,那么会暂停调用方协程,这下明白了吧。

不知道你有没有发现,上面我是调用GlobalScope.async方法而不知直接调用async方法,为什么要这么做呢?可不可以改成直接调用async呢?

首先,为什么要这么做?虽然答案很简单,但是这涉及到了下小节才会讲到的协程调度器。这里就先不讲调度器,直接说结果,由于runBlocking是运行在main线程的,也就是运行在单线程中,如果直接调用async创建协程,那么async创建的协程会继承runBlocking的上下文,也运行在main线程,这样的话,就会出现像单核cpu建多个线程执行任务一样,如果任务属于IO操作这样的非密集计算任务,那么单核cpu运行多线程确实可以提高效率,但如果是计算密集型任务,开多个线程去处理这样的任务只是在把cpu资源浪费在线程切换上,最终效率还不如单线程。单个线程中开多个协程也是这个道理。

而调用GlobalScope.async方法创建的协程是运行在Kotlin的默认调度器的线程池中(kotlin协程可以说是对线程(池)的封装),而非main方法所在线程。所以这里是调用GlobalScope.async而不直接调用async。解释了这么久,最后还是看一段代码直观体验一下吧:

fun main() =  runBlocking {
    val compute = {
        println("compute ${Thread.currentThread().name}")
        var count = 0L
        repeat(Int.MAX_VALUE) {
            count++
        }
        count
    }
    val timeMillis1 = measureTimeMillis {
        val first = compute()
        val second = compute()
        println("1 first + second = ${first + second}")
    }

    val timeMillis2 = measureTimeMillis {
        val first = async {
            compute()
        }.await()
        val second = async {
            compute()
        }.await()
        println("2 first + second = ${first + second}")
    }

    val timeMillis3 = measureTimeMillis {
        val first = async {
            compute()
        }
        val second = async {
            compute()
        }
        println("3 first + second = ${first.await() + second.await()}")
    }

    println("timeMillis1 = $timeMillis1")
    println("timeMillis2 = $timeMillis2")
    println("timeMillis3 = $timeMillis3")
}

执行结果:

compute main
compute main
1 first + second = 4294967294
compute main
compute main
2 first + second = 4294967294
compute main
compute main
3 first + second = 4294967294
timeMillis1 = 45
timeMillis2 = 81
timeMillis3 = 74

可以看到无论是在哪个协程中,compute的执行所在线程都是main线程,从中也可以看出所有协程都是跑在main线程中的,在单线程中做密集计算任务显然不使用协程效率是最高的。async就介绍到这,下面将会介绍到协程调度器,你准备好了吗?

withContext 你叫我去哪,我就去哪

在Kotlin中,协程的执行线程是受调度器控制的,那么调度器是个什么东西呢?该怎么使用呢?其实你可以把调度器当作十字路口的指挥车辆通行的交警,协程就是行驶的车辆,车辆需要遵从交警的指挥。这样就能保证道路的通畅。协程和调度器的关系就是这么回事。Talk is cheap, show me the code. 好,先看一段代码:

fun main() = runBlocking {
    println("runBlocking: ${Thread.currentThread().name}")
    withContext(Dispatchers.Default) {
        println("withContext: ${Thread.currentThread().name}")
    }
    println("runBlocking: ${Thread.currentThread().name}")
}

执行结果:

runBlocking: main
withContext: DefaultDispatcher-worker-1
runBlocking: main

从打印的结果可以看出,withContext并没有运行在main线程中,而是一个名为“DefaultDispatcher-worker-1”的线程。这个线程是Kotlin协程默认调度器所关联的线程池中的一个线程。这就是调度器的作用,那么这是怎么做到的呢,认真的同学一定发现了,withContext传入了一个Dispatchers.Default,这就是关键所在,我们先看一下Dispatchers.Default的定义:

public actual object Dispatchers {
    ...
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    ...
}

可以看到Dispatchers.Default其实是一个CoroutineDispatcher实例,没错,这就是协程中的调度器。先把Dispatchers.Default放一边。来看一看withContext的方法定义:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

来,仔细对比一下withContext和runBlocking的方法定义,你有没有发现他们的不同之处。来,一起来分析一下。

首先,withContext是用suspend修饰的,而runBlocking则不是,launch和async也没有用suspend修饰,相信你应该知道了吧,withContext也只能在suspend方法中被调用。并不能像runBlocking那样哪都能调用。

其次,我们来看一下它们的第一个参数,参数类型都是CoroutineContext,只不过withContext的第一个参数没有默认值,所以必须手动传入,而runBlocking中是有默认值的,而我们之前使用runBlocking都是没有传第一个参数,所以就是使用的默认值。

聪明的小伙伴一定能想到,既然withContext和runBlocking的第一个参数类型是一样的,withContext通过传入Dispatchers.Default能够运行在别的线程,那么runBlocking,launch和async是不是也可以呢?当然可以,不过,你有没有发现这里的第一个参数类型是CoroutineContext,而Dispatchers.Default的类型是CoroutineDispatcher,这两个类型能一样吗?少侠好眼力,类型确实不一样,但你只要去看看CoroutineDispatcher的类继承结构就会发现其实是下面这样的:
Kotlin中的协程,用起来原来这么简单?_第1张图片

其中的Element是CoroutineContext的内部的一个接口同时Elemnet继承自CoroutineContext。虽然继承层次有点多,但不难看出CoroutineDispatcher是一个CoroutineContext的派生类。这里只要了解一下就行了。

扯远了,我们接着讲withContext,既然让协程运行在不同线程并不是只有withContext才能做到的,那么withContext有什么特殊之处值得把它拎出来单独讲呢?还记得本小节开头的例子吗?输出结果是按照代码书写顺序输出的。你觉得会是偶然吗?不,这不是偶然事件,这是必然的。为什么这么说呢?来看一段withContext方法的官方注释:

Calls the specified suspending block with a given coroutine context,
suspends until it completes, and returns the result.

大致意思是调用withContext后会在指定的协程上下文中运行特定的可暂停代码块,并且会暂停调用协程直到withContext完成并返回结果。

这下知道withContext的特别之处了吧,啥,这个和runBlocking有啥区别?问得好,我们先看看runBlocking的官方注释:

Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.

大致意思是调用runBlocking后会运行一个新的协程并且可中断地阻塞当前线程直到它完成

对比一下,withContext是暂停当前协程,而runBlocking是阻塞当前线程。这下知道二者的区别了吧。

可能你对协程暂停还不是很清楚,那下面就来看个例子吧:

fun testSuspendContext() = runBlocking {
    val job = launch {
        println("launch开始: ${System.currentTimeMillis()}")
        while (isActive) {
            delay(500)
            println("hello")
        }
        println("launch开始: ${System.currentTimeMillis()}")
    }
    val first = withContext(Dispatchers.Default) {
        println("first开始: ${System.currentTimeMillis()}")
        var count = 0L
        repeat(1000) {
            count++
        }
        count.also {
            println("first结束: ${System.currentTimeMillis()}")
        }
    }
    val second = withContext(Dispatchers.IO) {
        println("second开始: ${System.currentTimeMillis()}")
        delay(2000)
        2000.also {
            println("second结束: ${System.currentTimeMillis()}")
        }
    }

    println("first + second = ${first + second}")
    job.cancel()
}

执行结果:

launch开始: 1588480873929
first开始: 1588480873930
first结束: 1588480873931
second开始: 1588480873934
hello
hello
hello
hello
second结束: 1588480875936
first + second = 3000

我们来一点一点分析,先看代码,我们先是调用launch创建了一个子协程,让它每500毫秒打印一个“hello”,并用job变量保存返回的Job对象,然后调用withContext并传入了Dispatchers.Default让它运行在默认调度器的线程池中,并且用first变量保存它返回的值,接着又调用withContext并传入Dispatchers.ID,让它运行在IO调度器的线程池中。并且用second变量保存它的返回值。接着打印first + second的和,最后调用job.cancel()取消job对应的协程。整个代码逻辑已经讲完了。

接下来我们来看输出,从输出可以发现“first开始: 1588480873930""first结束: 1588480873931”是紧挨着输出的,这也符合withContext的特性,它会暂停调用协程直到withContext执行结束。接着是打印了“second开始: 1588480873934”,但在“second结束: 1588480875936”打印之前打印了4次"hello", job是在runBlocking中调用launch创建的协程,因此,它时一个子协程,并且与runBlocking运行在同一个线程中,而second还在计算时是会暂停调用方协程的,也就是runBlocking会被暂停,从中可以知道,协程暂停并不会导致线程阻塞。

暂停是Kotlin协程中的一种状态,阻塞是线程的一种状态,因此二者不能混作一谈。现在应该能更好地区分runBlocking和withContext了吧。现在再看withContext其实也不过如此,对吧。现在你应该对协程的暂停有了自己的一些理解了吧。那我们一起来看下一节吧!

suspend(Cancellable)Coroutine,你站这别动,我好了会叫你的

上节讲了withContext的用法,你应该知道withContext会暂停当前协程直到withContext创建的协程完成并返回,suspendCoroutine和withContext都会暂停调用方协程,并且都有返回值,它们的相似点就这么点。下面将将它们的不同之处,withContext可以通过指定调度器,而suspendCoroutine则不能。withContext的返回只能在代码块内部,而suspendCoroutine则可以通过和异步回调结合使用以实现将返回控制权交给外部程序。

先从简单的开始,先来讲讲suspendCoroutine的简单用法,来,上代码:

fun main() = runBlocking {
    val first = suspendCoroutine<Int> {
        it.resume(10)
    }
    println(first)
}

执行结果:

10

注意it.resume(10),这句代码的作用就是让暂停的协程运行起来。如果不调用it.resume(10),那么runBlocking将一直处于暂停状态,永远不会结束。suspendCoroutine的使用方式就是这么简单,相信这对你来说已经没什么难度了吧。接下来讲讲它的进阶用法,还是从代码入手:

class Action(val callback: MyCallback) : Thread() {
    override fun run() {
        sleep(1000)
        if (Random.nextBoolean()) {
            callback.onSuccess(10)
        } else {
            callback.onFailed(Throwable())
        }
    }

    interface MyCallback {
        fun onSuccess(result: Int)
        fun onFailed(th: Throwable)
    }
}

suspend fun runBackgroundTask() = suspendCoroutine<Int> {
    Action(object : MyCallback {
        override fun onSuccess(result: Int) {
            it.resume(result)
        }

        override fun onFailed(th: Throwable) {
            it.resumeWithException(th)
        }
    }).start()
}

fun main() = runBlocking {
    val result = try {
        runBackgroundTask()
    } catch (th: Throwable) {
        println("有异常抛出")
        return@runBlocking
    }
    println(result)
}

执行结果:

有异常抛出

我运气有点背,运行了五次有四次抛出了异常。不过无伤大雅,重点不在结果,在于如何通过使用suspendCoroutine简化回调问题,其实上面还只是很简单的封装,在安卓中suspendCoroutine可以配合扩展方法来与OkHttp结合使用。这里就不再继续了,有兴趣的小伙伴可以自行研究。

讲完了suspendCoroutine后,再讲一下与之相似的suspendCancellableCoroutine,从名字应该就能看出它是可以被取消的类型,来,看一段代码:

fun main() = runBlocking {
    val result = try {
        suspendCancellableCoroutine<Int> {
            if (Random.nextBoolean()) {
                it.resume(10)
            } else {
                it.cancel()
            }
        }
    } catch (e: CancellationException) {
        println("取消了")
        return@runBlocking
    }
    println(result)
}

执行结果:

取消了

我们重点关注it.cancel()这个方法就是用来取消协程的,讲suspendCoroutine的时候,已经知道了调用it.resume()会唤起调用方协程并将结果返回。那么调用了it.cancel()会怎样呢?相信认真的小伙伴已经看出来了。没错,suspendCancellableCoroutine返回并会抛出CancellationException异常,其实并不是说抛出的一定是CancellationException异常,抛出什么异常取决于调用it.cancel()时传入的是什么异常,而这里啥也没传,就会抛出默认的CancellationException异常。suspendCancellableCoroutine就讲到这,把它当作suspendCoroutine的可取消版本使用即可。

使用总结

runBlocking 不要随意使用,一般用来和main方法一起用用就行了。

launch 需要启动一个无返回值的后台任务时使用。

async 当有多个有返回值的任务且可并发进行时,可以通过调用async创建多个协程让任务并发进行。

withContext 当需要执行一个带返回值的任务,并且该任务需要在指定执行环境时,可以使用withContext

suspend(Cancellable)Coroutine 可以用于简化异步回调

总结

虽然只是讲了几个常用的Kotlin协程用法,但是内容确多得出乎了我的意料。不过通过写博客可以让我加深对知识的理解和运用,也算是很有收获吧。

终于写完了,第一次写博客,没想到这么累,以前看别人的博客的时候只觉得别人的博客好长,现在才体会到写这么长的博客有多累。本来这篇博客应该昨天就写完了的,由于没有开自动保存,中途出了一次意外,写了一下午的内容直接没了,我当时整个人都是崩溃的。

最后还是得说一下,如果文章中有啥错误,还请各位不吝赐教。谢谢各位了!

你可能感兴趣的:(Kotlin中的协程,用起来原来这么简单?)