Kotlin——协程基础篇

官网地址
http://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html

第一次听到“协程”这两个字,立马就想到了进程和线程,看着很像,那他们之间有什么关系呢?
先看下协程相关的定义:
官方描述:协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程基础篇中,我们可以看到,“协程是轻量级的线程。 它们在某些 CoroutineScope 上下文中与 launch 协程构建器 一起启动。 这里我们在 GlobalScope 中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。”这样的一段描述。

大致对协程有了个模糊的印象后,先复习一下进行和线程吧。

什么是进程呢?
进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。

什么是线程呢?
线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程有自己的堆栈控件。

线程从创建到销毁会经过5个生命周期的阶段:
1、新建(new):线程被创建(new)的时候,线程就处于了新建状态;
2、可运行(runnable):当线程调用了star()方法之后,线程就处于了可运行状态;该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 ;
3、运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码,这是线程就处于运行中状态;
4、阻塞(blocked):当线程因为某种原因放弃了cpu 使用权,也就让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。这时候线程处于阻塞状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
5、死亡(dead):线程run()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。此时线程处于死亡状态。死亡的线程不可再次复生。
在这里插入图片描述

网上有人说:
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。

进程和线程的痛点
线程之间是如何进行协作的呢?
最经典的例子就是生产者/消费者模式:
若干个生产者线程向队列中写入数据,若干个消费者线程从队列中消费数据。
下图摘自网络
在这里插入图片描述
如何用java语言实现生产者/消费者模式呢?
让我们来看一看代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


public class ProducerConsumerTest {
    public static void main(String args[]) {
        final Queue<Integer> sharedQueue = new LinkedList();
        Thread producer = new Producer(sharedQueue);
        Thread consumer = new Consumer(sharedQueue);
        producer.start();
        consumer.start();
    }
}

class Producer extends Thread {
    private static final int MAX_QUEUE_SIZE = 5;

    private final Queue sharedQueue;

    public Producer(Queue sharedQueue) {
        super();
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (sharedQueue) {
                while (sharedQueue.size() >= MAX_QUEUE_SIZE) {
                    System.out.println("队列满了,等待消费");
                    try {
                        sharedQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                sharedQueue.add(i);
                System.out.println("进行生产 : " + i);
                sharedQueue.notify();
            }
        }
    }
}
class Consumer extends Thread {private final Queue sharedQueue;
    public Consumer(Queue sharedQueue) {
        super();
        this.sharedQueue = sharedQueue;
    }

    @Override
    public void run() {
        while(true) {
            synchronized (sharedQueue) {
                while (sharedQueue.size() == 0) {
                    try {
                        System.out.println("队列空了,等待生产");
                        sharedQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                int number = sharedQueue.poll();
                System.out.println("进行消费 : " + number );
                sharedQueue.notify();
            }
        }
    }
}

这段代码做了下面几件事:
1.定义了一个生产者类,一个消费者类。
2.生产者类循环100次,向同步队列当中插入数据。
3.消费者循环监听同步队列,当队列有数据时拉取数据。
4.如果队列满了(达到5个元素),生产者阻塞。
5.如果队列空了,消费者阻塞。

上面的代码正确地实现了生产者/消费者模式,但是却并不是一个高性能的实现。为什么性能不高呢?原因如下:
1.涉及到同步锁。
2.涉及到线程阻塞状态和可运行状态之间的切换。
3.涉及到线程上下文的切换。
以上涉及到的任何一点,都是非常耗费性能的操作。

为了更好的解决上面的问题,所以引出了协程的概念。

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。所以协程的开销是小于线程的。

进程、线程、协程的关系:
在这里插入图片描述

使用
Kotlin的协程,英文字母coroutine,封装在kotlinx-coroutines-core库中。
因此要使用协程,需要先在项目的build.gradle文件中导入相关依赖
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2’

我们可以通过以下方式创建一个协程
1、GlobalScope.launch { // 在后台启动一个新的协程
delay(1000L)
}
2、runBlocking { // 这个表达式构建的协程是阻塞式的,阻塞了主线程。
delay(1000L)
}
调用了runBlocking的主线程会一直阻塞直到runBlocking内部的协程执行完毕。
当我们使用GlobalScope.launch的时候,我们会创建一个顶层协程。虽然它很轻量,但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? 必须手动保持对所有已启动协程的引用并join很容易出错。

先看一下launch函数,源码如下:
在这里插入图片描述
1、首先通过传入 CoroutineContext作为参数值,由于我们直接调用,没有传入 CoroutineContext,则使用默认的EmptyCoroutineContext,作为参数值。
2、根据 CoroutineStart 的模式,确认当前的 Coroutine 是 StandaloneCoroutine 还是 LazyStandaloneCoroutine。
3、最后通过调用 coroutine.start(start, coroutine, block)方法,执行协程。

runBlocking官方文档中的说明如下:
在这里插入图片描述
运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(suspending style)编写的库,以用于主函数与测试。
协程中使用suspend修饰符修饰一个函数,表示该函数为挂起函数,从而运行在协程中。挂起函数,它不会造成线程阻塞,但是会挂起协程,并且只能在协程中使用。挂起函数不可以在main函数中被调用,所以就是使用runBlocking函数!
在这里插入图片描述

在上面我们还可以看到协程里面调用了一个delay()方法。查看源码得知,delay是将协程延迟给定的时间,但并不会阻塞线程。
在这里插入图片描述

作用域
下面通过一段代码了解一下协程的作用域。
在这里插入图片描述
日志输入如下:
在这里插入图片描述
通过日志的输出,我们可以了解到,函数runBlocking会阻塞当前的线程,在runBlocking中,挂起函数coroutineScope会优先launch函数执行,coroutineScope内部常规代码也会优先于launch内部的代码执行。

这时候如果将launch中的代码块,抽取为一个函数的话,由于delay是一个挂起函数,所以抽取的函数需要带上“suspend”修饰符。
在这里插入图片描述
如果提取出的函数包含一个在当前作用域中调用的协程构建器的话,该怎么办? 在这种情况下,所提取函数上只有suspend修饰符是不够的。为CoroutineScope写一个doSomething扩展方法是其中一种解决方案,但这可能并非总是适用,因为它并没有使API更加清晰。惯用的解决方案是要么显式将CoroutineScope作为包含该函数的类的一个字段,要么当外部类实现了CoroutineScope时隐式取得。作为最后的手段,可以使用CoroutineScope(coroutineContext),不过这种方法结构上不安全,因为你不能再控制该方法执行的作用域。只有私有API 才能使用这个构建器。
在这里插入图片描述
在这里插入图片描述

以下两段代码,分别启动了 1 万个协程和1万个线程,打印出耗时时间。
在这里插入图片描述
在这里插入图片描述

通过控制台可以看到协程和线程的耗时分别为3秒和27秒。
在这里插入图片描述

全局协程
通过GlobalScope.launch开启的协程,是一个顶层协程,也叫全局协程。通过每秒输出“I’m sleeping”两次,来让协程长期运行,之后在主函数中延迟一段时间后返回。
在GlobalScope中启动的活动协程并不会使进程保活。它们就像守护线程。
在这里插入图片描述
在这里插入图片描述

在Android的activity或者fragment页面中,由于无法将生命周期的函数添加“suspend”修饰符,所以只能使用GlobalScope.Launch等方式嵌套,如:
在这里插入图片描述
此时通过上述嵌套的方式虽然执行了delay(1300L),但是launch内部的repeat并不会被取消,任然会继续执行知道1000次之后
在这里插入图片描述
在这里插入图片描述
这时候就涉及到协程的取消和超时了。

取消与超时
在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。该launch函数返回了一个可以被用来取消运行中的协程的Job。

取消
要想取消一个协程,我们可以将上面的代码修改一下,将整个launch赋值给一个job,通过job.cancel来控制协程的取消。
在这里插入图片描述
在这里插入图片描述

协程的取消是协作的。一段协程代码必须协作才能被取消。所有kotlinx.coroutines中的挂起函数都是可被取消的。它们检查协程的取消,并在取消时抛出CancellationException。然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:
在这里插入图片描述
在这里插入图片描述
通过日志可以看到,协程被取消后,仍然继续把5次循环都执行完毕之后才结束。

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的yield是一个好的选择。另一种方法是显式的检查取消状态。让我们试试第二种方法。
将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

此时,循环就被取消了,不会再出现取消后还在继续执行的情况。两种方法实现效果第一致的。

通常使用如下的方法处理在被取消时抛出CancellationException的可被取消的挂起函数。比如说,try {……} finally {……}表达式以及 Kotlin 的use函数一般在协程被取消的时候执行它们的终结动作:
在这里插入图片描述

当你需要挂起一个被取消的协程,你可以将相应的代码包装在withContext(NonCancellable) {……}中,并使用withContext函数以及NonCancellable上下文,见如下示例所示:
在这里插入图片描述
在这里插入图片描述

超时
绝大多数取消一个协程的理由是它有可能超时。当你手动追踪一个相关Job的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用withTimeout函数来做这件事。 来看看示例代码:
在这里插入图片描述
在这里插入图片描述
超时后,repeat里的代码不再继续执行。

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {…} catch (e: TimeoutCancellationException) {…} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // 在它运行得到结果之前取消它
}
println("Result is $result")

输出:
I’m sleeping 0 …
I’m sleeping 1 …
I’m sleeping 2 …
Result is null

组合挂起函数
我们在一个协程中,顺序的调用两个挂起函数,发现在协程中他们的执行顺序是默认的顺序调用。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过上面的日志,我们可以看到上面两个函数式的计算花费了两秒的时间。
如果的doSomethingOne和doSomethingTwo两个函数之间没有依赖的关系,并且我们想更快的得到结果,让它们进行并发吗?这就是async可以帮助我们的地方。
看下面一段代码:
我们对doSomethingOne和doSomethingTwo这两个函数做了一点变形,通过async来启动一个单独的协程,这是一个轻量级的协程,可以与其他的协程一起并发工作,与launch的不同之处在于launch返回一个Job并且不附带任何结果值,而async返回一个Deferred —— 一个轻量级的非阻塞future,这代表了一个将会在稍后提供结果的promise。你可以使用 .await() 在一个延期的值上得到它的最终结果,但是Deferred也是一个Job,所以如果需要的话,你可以取消它。
先看下async的源码:
在这里插入图片描述
1、与launch类似,可以传入 CoroutineContext作为参数值,由于我们直接调用,没有传入 CoroutineContext,则使用默认的EmptyCoroutineContext,作为参数值。
2、根据CoroutineStart的模式,确认当前的Coroutine是StandaloneCoroutine还是LazyStandaloneCoroutine。
3、最后通过调用 coroutine.start(start, coroutine, block)方法,执行协程。
首先使用默认模式:
在这里插入图片描述
在这里插入图片描述
通过上面代码,我们可以得知,两个协程并发执行,时间上比顺序执行快了一倍。
注意,使用协程进行并发总是显式的。

然后使用async模式:
设置start模式为惰性的CoroutineStart.LAZY。只有结果通过await获取的时候协程才会启动,或者在Job的start函数调用的时候。运行下面的示例:
在这里插入图片描述

上面的协程,只有在我们调用了start()的时候,one和two两个协程才会执行。
在这里插入图片描述
在这里插入图片描述

如果我们只调用了await()却没有调用start(),那么这样的执行顺序与默认的顺序执行是类似的。直到await()启动该协程执行并等待至它结束,这并不是惰性的预期用例。在计算一个值涉及挂起函数时,这个async(start = CoroutineStart.LAZY)的用例用于替代标准库中的lazy函数。

async 的结构化并发
当async被定义为了CoroutineScope上的扩展,我们需要将它写在作用域内,并且这是coroutineScope函数所提供的:
这种情况下,如果在concurrentSum函数内部发生了错误,并且它抛出了一个异常,所有在作用域中启动的协程都会被取消。如下代码:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值