要说程序如何从简单走向复杂, 线程的引入必然功不可没, 当我们期望利用线程来提升程序效能的过程中, 处理线程的方式也发生了从原始时代向科技时代发生了一步一步的进化, 正如我们的Elisha大神所著文章The Evolution of Android Network Access中所讲到的, Future可能会是Kotlin Coroutines的时代.

什么是Coroutines

Coroutines是Kotlin 1.1推出的实验性的一个扩展, 它被定义为一个轻量级的高效的线程框架, 并且在1.3版本正式发布, 去掉Experiment标签.

如何启动一个Coroutines

最基础的创建一个Coroutines的方法就是使用launch或者async, 二者的区别是前者返回的是一个Job, 不带结果 而后者可以将结果以Deferred<T>格式返回.

如:

  1. val job = launch {
  2. delay(100)
  3. }

而通常在Coroutines内执行的函数都会有一个suspend声明, 而有suspend声明的函数也只能在Coroutines Scope中调用.

suspend的意思是这个函数可以被suspend(挂起), 让Coroutines来调度它, 这也是为何Kotlin的delay函数可以不阻塞的进行延迟, 因为它就是一个suspend函数.

Coroutines与线程的关系

Coroutines可以简单理解为一个有队列的任务链, 每一个Coroutines都有自己的Context, 而Context又可以决定其运行的线程.

所以可以看到, 并不是起一个Coroutines就是起了一个线程, 而只是启动了一个在某个Scope下运行的协程(Coroutines)罢了. 这里的Scope (CoroutineScope) 内部包含了一个 Context (CoroutineContext).

  1. public interface CoroutineScope {
  2. public val coroutineContext: CoroutineContext
  3. }

如果只是通过launch来启动一个协程, 那它将会运行在Parent Scope所定义的线程中, 但是如果使用GlobalScope.launch来启动一个协程, 它将会使用线程池中的线程来创建一个协程, 线程池的大小跟CPU的核数相关.

当然launch也支持自己传入一个CoroutinesContext来控制它运行的线程, 它叫做CoroutineDispatcher, 是Context的子类.

上面讲了默认的launch会启在父Scope(Context)的线程中, 而launch(Dispatchers.Default)则等于GlobalScope.launch, 还可以通过launch(newSingleThreadContext("MyOwnThread"))来启动自己的线程, 另外有一个不推荐在general code中出现的launch(Dispatchers.Unconfined), 它将会运行在第一个进入Suspend状态的线程中.

可以举一个简单的例子:

  1. val job = launch {
  2. log("hehe")
  3. delay(1000)
  4. log("haha")
  5. }

这个协程是可以完全在main函数里执行完的, 即输出结果为:

  1. hehe
  2. haha

因为launch会跑在main的scope中. 如果替换成:

  1. val job = GlobalScope.launch {
  2. log("hehe")
  3. delay(1000)
  4. log("haha")
  5. }

则只会输出hehe, 因为主线程已经结束.

这里我们可以通过job.join()来等待子协程执行结束, 这一点跟大家熟知的线程的join是一样.

如何切换Context

如果把Context对应到我们平时认为的线程, 那么这个问题可以类比成 如何切换线程.

答案是使用withContext, 举一个简单的栗子.

  1. launch(UI) {
  2. updateUI()
  3. val result = withContext(IO) {
  4. }
  5. setView(result)
  6. }

它类似于async(IO){ }.await().

如何共享资源

线程与线程之间会涉及到同步与资源竞争的关系, 协程亦是如此.

通常情况下在线程中我们解决问题的方式是加锁, 而不正确的使用可能会导致性能下降甚至死锁(dead lock. 或者在高级语言中使用已经实现线程安全的数据类型, 来进行夸线程操作。

而我们的Coroutines自然也考虑到了这一点, 它认为我们不应该以共享资源来进行通信, 而是以通信来进行资源共享.

  1. Do not communicate by sharing memory; instead, share memory by communicating.

所以它提出了一个叫做Channel的东西来在不同的Coroutines之间进行通信.

譬如我们期望将一堆数据交给两个并行的协程进行处理, 那么我们可以把数据放进Channel, 其他的协程从这个Channel进行数据读取.

  1. launch {
  2. for (o in data) { channel.send(o) }
  3. channel.close()
  4. }
  5. launch(One) {
  6. for (o in channel) {
  7. xxx
  8. }
  9. }
  10. launch(Two) {
  11. for (o in channel) {
  12. xxx
  13. }
  14. }

一定要记得关闭channel, 否则从channel读取数据的协程都将会无限挂起等待数据传过来.

由于Channel本身实现了iterator, 所以直接通过in就可以挨个取出内部的数据.

ReceiveChannel与SendChannel

上一个环节提到的协程之间是通过Channel来进行通信, 而Channel本身却是实现了接收管道与发送管道两个接口.

我们可以通过producer函数来进行生成数据, 提供给别的协程, 因为它的返回值是一个ReceiveChannel.

  1. val channel = produce<XXX>() {
  2. for (o in data) send(o)
  3. }

而且produce自己会做channel close的处理, 省去我们发送完毕还要掉close的烦恼.

如果我们多个协程需要发送请求并集中处理, 或者可以叫数据整合, 那么我们可能需要用到actor这个函数, 它的返回值是一个SendChannel.

  1. val channel = actor<XXX>() {
  2. consumeEach {
  3. xxx
  4. }
  5. }
  6. launch(One) {
  7. channel.send(xxx)
  8. }
  9. launch(Two) {
  10. channel.send(xxx)
  11. }

由于actor返回的SendChannel有点像是一个邮箱, 它会不断的接收数据, 所以必须手动关闭才会停止.

多个Channel之间数据如何进行选择

Coroutines推出一个仍在Experiment阶段的关键字select来在多个suspend function中进行选择第一个到达available的, 其实有点像RxJava的concat+first.

比如我有两个接收Channel, 但是每一个Channel接收到数据的频率不得而知, 我想分别从中得到数据, 这里就需要使用select.

  1. select<Unit> {
  2. channel1.onReceive {}
  3. channel2.onReceive {}
  4. }

如果在配合外围的循环, 就可以做到不断的去接收两个Channel的数据.

再比如有两个发送Channel都可以处理我的需求, 我也不知道这个时候谁是空闲的, 那也可以通过select来解决.

  1. select<Unit> {
  2. channel1.onSend(xxx) {}
  3. channel2.onSend(xxx) {}
  4. }

有时候两个Channel是嵌套使用的.

比如一个咖啡店, 他们会不断的收到Oder, 只有两个打咖啡的服务员, 咖啡机也只有两个口, 如果我们对这个咖啡店进行抽象. 将Oder存在于一个Channel里, 服务员接收Order并不断的把咖啡递出来, 这也是一个Channel, 咖啡机会不断接收到服务员需要打咖啡的操作, 也这是一个Channel.

而在这个过程中, 两个服务员会有一个选择, 咖啡机的两个出口也会有一个选择的过程.

如果抽象成我们的Coroutines代码, 或许会是这个样子:

  1. val orderChannel = producer {
  2. for (o in orders) send(o)
  3. }
  4. val waiter1 = producer {
  5. for (o in orderChannel) {
  6. pullCoffee(o)
  7. }
  8. }
  9. // waiter2 is the same as 1
  10. val coffeePort1 = actor {
  11. consumeEach {
  12. //pass coffee through channel inside order
  13. it.channel.send(Coffee)
  14. it.channel.close()
  15. }
  16. }
  17. // coffeePort2 is the same as 2
  18. pullCoffee {
  19. select<Coffee> {
  20. coffeePort1.onSend(Request(channel)) {
  21. //get coffee from coffeePort
  22. channel.recevie()
  23. }
  24. coffeePort2.onSend ....
  25. }
  26. }
  27. while(someCondition) {
  28. select<Coffee> {
  29. waiter1.onReceiveOrNull {
  30. //上菜了
  31. }
  32. waiter2.onReceiveOrNull {
  33. //上菜了
  34. }
  35. }
  36. }

补充说明

协程作为未来non blocking编程的方向, 需要大家花时间去理解, 花时间去尝试, 在此特别推荐这个咖啡小程序帮助大家学习.

https://medium.com/@jagsaund/kotlin-coroutines-channels-csp-android-db441400965f

以及官方的Overview

https://kotlinlang.org/docs/reference/coroutines-overview.html

还有个CheatSheet可以参考

https://blog.kotlin-academy.com/kotlin-coroutines-cheat-sheet-8cf1e284dc35

探究高级的Kotlin Coroutines知识的更多相关文章

  1. Kotlin Coroutines在Android中的实践

    Coroutines在Android中的实践 前面两篇文章讲了协程的基础知识和协程的通信. 见: Kotlin Coroutines不复杂, 我来帮你理一理 Kotlin协程通信机制: Channel ...

  2. Kotlin Coroutines不复杂, 我来帮你理一理

    Coroutines 协程 最近在总结Kotlin的一些东西, 发现协程这块确实不容易说清楚. 之前的那篇就写得不好, 所以决定重写. 反复研究了官网文档和各种教程博客, 本篇内容是最基础也最主要的内 ...

  3. pythonl练习笔记——爬虫的初级、中级、高级所匹配的知识

    1 初级爬虫 (1)Web前端的知识:HTML, CSS, JavaScript, DOM, DHTML, Ajax, jQuery,json等: (2)正则表达式,能提取正常一般网页中想要的信息,比 ...

  4. Kotlin基础知识

    1. 改进点/基础 //安全判空 val length = text?.length; //类型转换 if (object is Car) { var car = object as Ca } //操 ...

  5. kotlin 冷知识 *号 展开数组

    Kotlin笔记-冷门知识点星号(*) 2019年05月10日 11:37:00 weixin_33724059 阅读数 6   可变参数展开操作符 在数组对象前加*号可以将数组展开,方便传值,比如: ...

  6. JS高级程序设计2nd部分知识要点7

    例子: <!DOCTYPE html> <html lang="en"> <head>  <meta charset="UTF- ...

  7. JS高级程序设计2nd部分知识要点6

    DOM nodeType属性 所有类型节点都有的两个方法 1. cloneNode()用于创建调用这个方法的节点的一个完全相同的副本.

  8. JS高级程序设计2nd部分知识要点5

    JS Regexp 字面量模式 用\反斜杠转义 构造函数中的字符串 也用\转义正则也用\ RegExp实例属性 global -布尔值  /g ignoreCase -布尔值 /i lastIndex ...

  9. JS高级程序设计2nd部分知识要点4

    ECMAScript中所有函数的参数都是按值传递的. 5种基本数据类型: Undfined,Null,Boolean,Number,String. ECMAScript中的所有参数传递的都是值,不可能 ...

随机推荐

  1. Http 1.x弊端与Http 2.0比较

    HTTP2.0作为新版协议,改动细节必然很多,不过对应用开发者和服务提供商来说,影响较大的就几点. 新的二进制格式(Binary Format) http1.x诞生的时候是明文协议,其格式由三部分组成 ...

  2. [Swift]LeetCode377. 组合总和 Ⅳ | Combination Sum IV

    Given an integer array with all positive numbers and no duplicates, find the number of possible comb ...

  3. [Swift]LeetCode906. 超级回文数 | Super Palindromes

    Let's say a positive integer is a superpalindrome if it is a palindrome, and it is also the square o ...

  4. 解决classNotFound的问题的思路

    用Ctrl+Shift+t可以查看class,对于报错信息,我们把没有找到的class放到查找框里进行查看,找到之后把这个jar包放到WEB-INF的lib目录下,build path一下就可以了. ...

  5. css奇淫巧计

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  6. Python之高级特性

    一.切片 L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']取出前三个元素 , 笨方法就是通过下标一个一个获取 [L[0], L[1], L[2]]Pyt ...

  7. HTTP 权威指南 详解 ( 一、概述 )

    HTTP 权威指南 详解 ( 一.概述 ) 最近在解读 <http权威指南> 这本书.之前对于http 的理解仅限于 知道我需要向服务端发送一个 get or post 请求,然后等待服务 ...

  8. Scrum到底是个神马玩意儿

    从前有一种非常火爆的体育运动,对阵双方各派出11位猛男,在宽阔的草皮球场内争抢一颗可怜的小皮球.哪方能够通过团队协作拿到皮球,并且运送到对方场地的特定位置即得分. 没错,你没有走错片场,快到超级碗里来 ...

  9. Java虚拟机详解----JVM内存结构

    http://www.cnblogs.com/smyhvae/p/4748392.htm 主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启 ...

  10. HotSpot虚拟机对象相关内容

    一.对象的创建 1.类加载检查 普通对象的创建过程:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载.解析和初始化 ...