Kotlin从入门到放弃(三)——协程

引言

这篇主要是将以下kotlin里面的协程,当然这个概念已经随着kotlin的文档被广泛得知了,不过还是用大量代码记录一下吧

一、概念

   Coroutine,翻译为协程,意思为各个子任务程协作运行。由此可以联想到Java常用的线程概念,java中的线程Thread最终启动的地方是JVM核心层,也就是说java的线程其实本质也是和硬件有关(这是当然的)。而多线程任务在并发的情况下会出现阻塞的情况,协程提供了挂起这种方法去避免阻塞线程并用更廉价更可控的操作替代线程阻塞。

二、入门

   协程是由程序直接实现的,是一种轻量级线程,kotlin也为此提供了标准库和额外的实验库。标准库为kotlin.coroutines.experimental(写作时使用kotlin-1.20版本),可见仍然还是一个实验性功能。其实协程的例子能在网上找到很多,就不一一去分析了,这里以标准库实现的斐波那契方法为例讲解一下。

val fibonacciSeq = buildSequence {
        var a : Long = 0
        var b : Long = 1
        yield(1)           // 1
        while (true) {
            yield(a + b)   // 2
            val tmp = a + b
            a = b
            b = tmp
            print(tmp.toString() + " ") // 3
        }
    }
println(fibonacciSeq.take(10).toList()) // 4

   上面的fibonacciSeq函数实现了斐波那契数列,但是不同于java语法写的方法这里多了一个buildSequence,buildSequence源码如下:

/**
 * Builds a [Sequence] lazily yielding values one by one.
 *
 * @see kotlin.sequences.generateSequence
 *
 * @sample samples.collections.Sequences.Building.buildSequenceYieldAll
 * @sample samples.collections.Sequences.Building.buildFibonacciSequence
 */
@SinceKotlin("1.1")
public fun <T> buildSequence(builderAction: suspend SequenceBuilder<T>.() -> Unit): Sequence<T> = Sequence { buildIterator(builderAction) }

   从官方的注释中可以知道,这是创建了一个惰性的序列,也就是函数fibonacciSeq是一个创建无穷惰性的斐波那契数列。注释1处的yield(1)作用就是输出1,yield的作用可以参考Python语言的相当于return;同样注释2处也同理;注释3和4处均是输出,不同的是4处可以主动取出有限的数列,两者的输出作为对比去理解yield在此处的return作用。

   上图可以明显看出注释3处少输出了一个元素,这是因为yield在第10次直接返回,跳出了while循环。构建这种无限序列(后面还会讲)是协程的一个主要特点,不过可能从这里很难看出它的概念(子任务),理解任务的调度可以引入kotlin提供的一个额外Coroutine库——kotlinx.coroutines.experimental,显而易见也是试验阶段。

三、实践

3.1 启动协程

   使用kotlinx库就需要gradle了(当然其他的如Maven也是可以的,手动滑稽),版本选择和导入不再赘述,还是直接上代码。

    // 在Common线程池启动协程
    launch(CommonPool) {
        delay(2000L)    // 1
        println("Hello")
    }
    println("World")
    Thread.sleep(3000L) // 2
    // 在主线程中启动协程
    runBlocking<Unit> {
        println("T0")
        launch(CommonPool) {
            println("T1")
            delay(3000L)
            println("T2 Hello")
        }
        println("T3 World")
        delay(5000L)
        println("T4")
    }

   此处将两个函数写在一处好做对比,1处的delay函数(非阻塞)是一个suspend(挂起)函数,在这里的作用相当于2处的Thread.sleep(阻塞),但是delay必须在协程中或者挂起函数中使用,但是lauch是在CommonPool共享线程池中创建协程并不是主线程,所以不能使用,解决的办法就是使用runBlocking——桥接普通阻塞代码和挂起风格的非阻塞代码,即可以在里面启动协程,使用挂起函数和常用的阻塞方法。

3.2 取消协程

   有启动自然就有取消操作,使用cancel函数,当然这里也是有需要注意的点。

runBlocking {
        val job = launch {
            repeat(1000) {
                i -> println("job sleeping $i ... CurrentThread: ${Thread.currentThread()}")
                delay(500L)
            }

        }

        val job1 = launch {
            var nextTime = 0L
            var i = 0
            while (i < 20) {
                var currentTime = System.currentTimeMillis();
                if (currentTime >= nextTime) {
                    println("job1 sleeping ${i++} ... CurrentThread: ${Thread.currentThread()}")
                    nextTime = currentTime + 500L
                }
            }
        }
        delay(1900L)
        println("Job is alive: ${job.isActive}; Job iscompleted: ${job.isCompleted}")
        println("Job1 is alive: ${job1.isActive}; Job1 iscompleted: ${job1.isCompleted}")
        val b1 = job.cancel()
        val c1 = job1.cancel()
        println("job cancel: $b1 and job1 cancel: $c1")
        delay(1300L)
        println("Job is alive: ${job.isActive}; Job iscompleted: ${job.isCompleted}")
        println("Job1 is alive: ${job1.isActive}; Job1 iscompleted: ${job1.isCompleted}")
        delay(30000L)
        val b2 = job.cancel()
        val c2 = job1.cancel()
        println("job cancel: $b2 and job1 cancel: $c2")
        println("Job is alive: ${job.isActive}; Job iscompleted: ${job.isCompleted}")
        println("Job1 is alive: ${job1.isActive}; Job1 iscompleted: ${job1.isCompleted}")
    }

   同样是两个协程job和job1,job是一个可重复1000次但会被挂载的协程而job1是一个有时间间隔循环20次的协程,这里就不截取输出的结果了因为比较长。协程job由于使用了delay函数挂起,在调用了cancel之后协程实现了真正的停止;协程job1存在循环计算并没有挂起操作,即使调用了cancel,协程状态也变为停止,但是循环操作仍然在继续,这种情况下取消就会失效。那遇到第二种情况该怎么办嘞,联系到上面讲过的yield函数(注意这里的yield函数是kotlinx包中的和上面那个重名),写在适当的地方就可以了(话说还不如直接用delay)。

3.3 等待协程

   之前的代码中实现的功能不同,但是有个共同的特点,那就是主线程主动挂起或者阻塞等待协程里面的代码执行,这在实际使用中肯定是不可取。kotlinx提供了join函数,可以使主线程等待协程执行完。

runBlocking {
    var c1 = launch(CommonPool) {
        delay(1000L)
        println("Coroutine 1")
    }
    var c2 = launch {
        delay(1000L)
        println("Coroutine 2")
    }
    c1.join() // 1
    c2.join() // 2
    println("the main")
}

   join函数也是一个挂起函数源码解析放在之后吧,如果没有1和2处的代码,整个程序的运行结果只有“the main”,而现在的执行结果如下图:

   上图的目的就是为了说明协程的执行也是无序的。

3.4 CommonPool线程池

   官方的解释是这样的Represents common pool of shared threads as coroutine dispatcher for compute-intensive tasks,机器翻译一下就是将共享线程池作为一个协程来调度计算密集型任务。注释1处会尝试新建一个ForkJoinPool(一个可执行ForkJoinTask的ExcuteService,采用工作窃取算法:所有在池中的线程尝试去执行其他线程创建的子任务,这样很少有线程处于空闲状态,更加高效);如果不可用,就是用Executors来创建一个普通的线程池,创建过程在注释3处。

    private fun createPool(): ExecutorService {
        val fjpClass = Try { Class.forName("java.util.concurrent.ForkJoinPool") }
            ?: return createPlainPool()   // 1
        if (!usePrivatePool) {
            Try { fjpClass.getMethod("commonPool")?.invoke(null) as? ExecutorService }
                ?.let { return it }
        }
        Try { fjpClass.getConstructor(Int::class.java).newInstance(defaultParallelism()) as? ExecutorService }
            ?. let { return it }
        return createPlainPool()  // 2
    }

    private fun createPlainPool(): ExecutorService {
        val threadId = AtomicInteger()
        return Executors.newFixedThreadPool(defaultParallelism()) {
            Thread(it, "CommonPool-worker-${threadId.incrementAndGet()}").apply { isDaemon = true }  // 3
        }
    }

    private fun defaultParallelism() = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1)

四、小结

   协程的概念和基本操作讲的差不多了,对于经常接触Thread线程突然去理解协程还是有点障碍的,不过官方提供了很好的试验库去入门。协程也不是一两篇文章就能讲清楚的,应该还会有下一篇继续介绍。

时间: 2024-10-27 06:59:58

Kotlin从入门到放弃(三)——协程的相关文章

Kotlin从入门到“放弃”(一)

Google2017年I/O大会圆满结束了,在此次大会上谷歌爸爸把未来的重点移到了人工智能上,继续主推自家的TensorFlow,对Android开发者来说唯一的一个惊喜就是Kotlin这门语言成为了官方承认的开发语言(当然我们不能忘了Android O).特意开了一个Kotlin的坑,记录自己学习的历程. 1.Kotlin简介      Kotlin是JetBrains公司开发,基于JVM的一种语言,官网说可以百分百兼容Java语言,它能够进行服务器端,Android端和web前端(兼容Jav

Kotlin从入门到“放弃”(二)——函数

写在开头 上一篇介绍了Kotlin的基本使用,发现这门语言主要还是面向函数进行编程,所以这一篇主要在函数方面介绍Kotlin. 基本函数   Kotlin作为一个面向函数的编程语言,函数的使用自然是最基本的,上一篇的main函数就是函数式语言的体现.最简单的调用函数的方法如下: fun main(vararg arg: String){ println(add(1, 2)) } fun add(a: Int, b: Int): Int{ return a + b }   由上面的自定义函数add

【译】第一次走进 Android 中的 Kotlin 协程

本文讲的是[译]第一次走进 Android 中的 Kotlin 协程, 原文地址:A first walk into Kotlin coroutines on Android 原文作者:Antonio Leiva 译文出自:掘金翻译计划 译者:Feximin 校对者:wilsonandusa .atuooo 本文提取并改编自最近更新的 Kotlin for Android Developers 一书. 协程是 Kotlin 1.1 引入的最牛逼的功能.他们确实很棒,不但很强大,而且社区仍然在挖掘

一个使用 asyncio 协程的网络爬虫(三)

使用协程 我们将从描述爬虫如何工作开始.现在是时候用 asynio 去实现它了. 我们的爬虫从获取第一个网页开始,解析出链接并把它们加到队列中.此后它开始傲游整个网站,并发地获取网页.但是由于客户端和服务端的负载限制,我们希望有一个最大数目的运行的 worker,不能再多.任何时候一个 worker 完成一个网页的获取,它应该立即从队列中取出下一个链接.我们会遇到没有那么多事干的时候,所以一些 worker 必须能够暂停.一旦又有 worker 获取一个有很多链接的网页,队列会突增,暂停的 wo

进程,线程,协程

最早出现的是进程,后来为了调度的方便出现了线程,现在又蹦出了一个协程.这到底是个什么东西呢. 并发和并行: 最早的计算机,每次只能执行一个程序,别的都得等着.到后来,计算机运算速度提高了,于是就想要同一时间执行那么三五个程序,几个程序能一块跑一跑.特别是UI什么的,别跑个程序得排队等着.于是就有了并发. 从程序员的角度可以看成是多个独立的逻辑流.把单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的.这个时候内存其实是共享的.后来一电脑上有了好几个cpu,好咧,大家都别闲着,可以一块跑.

python线程、进程和协程详解_python

引言 解释器环境:python3.5.1 我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句: server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer) ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver

PHP混合Go协程并发

想法很简单.通过设置 runtime.GOMAXPROCS(1) 让 golang 的进程变成单线程执行的.类似python用gevent的效果.然后通过调度多个协程实现异步I/O并发.php作为一个子函数跑在go的进程内,php需要yield到其他协程时,通过回调到golang函数来实现.从php里调用go提供的子函数时,go保证保存php的当前上下文.当协程执行权让渡回来的时候,把原来的php上下文恢复.关键的代码在:  // 保存当前协程上的php上下文     oldServerCtx 

[译]C语言协程

C语言协程 by Simon Tatham 原文链接:http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html 引言 为大型程序设计一个良好的结构通常是一件困难的事情.其中一个经常出现的问题是:如果你有一段代码产生数据,另一段代码消费数据,那么谁应该作为调用者,谁应该作为被调用者? 下面是一段很简单的Run-Length(游程编码)解压缩代码(Decompressor): /* Decompression code */ while

谈谈Python协程技术的演进

一.引言 1. 存储器山 存储器山是 Randal Bryant 在<深入理解计算机系统>一书中提出的概念. 基于成本.效率的考量,计算机存储器被设计成多级金字塔结构,塔顶是速度最快.成本最高的 CPU 内部的寄存器(一般几 KB)与高速缓存,塔底是成本最低.速度最慢的广域网云存储(如百度云免费 2T ) 存储器山的指导意义在于揭示了良好设计程序的必要条件是需要有优秀的局部性: 时间局部性:相同时间内,访问同一地址次数越多,则时间局部性表现越佳; 空间局部性:下一次访问的存储器地址与上一次的访