要说程序如何从简单走向复杂, 线程的引入必然功不可没, 当我们期望利用线程来提升程序效能的过程中, 处理线程的方式也发生了从原始时代向科技时代发生了一步一步的进化, 正如我们的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>格式返回.

如:

val job = launch {
delay(100)
}

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

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

Coroutines与线程的关系

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

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

public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

如果只是通过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状态的线程中.

可以举一个简单的例子:

val job = launch {
log("hehe")
delay(1000)
log("haha")
}

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

hehe
haha

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

val job = GlobalScope.launch {
log("hehe")
delay(1000)
log("haha")
}

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

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

如何切换Context

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

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

launch(UI) {
updateUI()
val result = withContext(IO) { }
setView(result)
}

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

如何共享资源

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

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

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

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

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

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

launch {
for (o in data) { channel.send(o) }
channel.close()
} launch(One) {
for (o in channel) {
xxx
}
} launch(Two) {
for (o in channel) {
xxx
}
}

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

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

ReceiveChannel与SendChannel

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

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

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

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

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

val channel = actor<XXX>() {
consumeEach {
xxx
}
} launch(One) {
channel.send(xxx)
}
launch(Two) {
channel.send(xxx)
}

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

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

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

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

select<Unit> {
channel1.onReceive {}
channel2.onReceive {}
}

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

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

select<Unit> {
channel1.onSend(xxx) {}
channel2.onSend(xxx) {}
}

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

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

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

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

val orderChannel = producer {
for (o in orders) send(o)
} val waiter1 = producer {
for (o in orderChannel) {
pullCoffee(o)
}
} // waiter2 is the same as 1 val coffeePort1 = actor {
consumeEach {
//pass coffee through channel inside order
it.channel.send(Coffee)
it.channel.close()
}
} // coffeePort2 is the same as 2 pullCoffee {
select<Coffee> {
coffeePort1.onSend(Request(channel)) {
//get coffee from coffeePort
channel.recevie()
}
coffeePort2.onSend ....
}
} while(someCondition) {
select<Coffee> {
waiter1.onReceiveOrNull {
//上菜了
}
waiter2.onReceiveOrNull {
//上菜了
}
}
}

补充说明

协程作为未来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. Android端IM应用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展

    本文由“猫爸iYao”原创分享,感谢作者. 1.引言 最近有个需求:评论@人(没错,就是IM聊天或者微博APP里的@人功能),就像下图这样:   ▲ 微信群聊界面里的@人功能    ▲ QQ群聊界面里 ...

  2. JDK设计模式之——装饰者模式

    假定已经有三个类A,B和C他们的继承关系如下          ClassA Class B extends A  Class C extends A  想进一步扩展类B和类C的功能,新增三个方法 m ...

  3. [Java]LeetCode141. 环形链表 | Linked List Cycle

    Given a linked list, determine if it has a cycle in it. Follow up:Can you solve it without using ext ...

  4. [Swift]LeetCode724. 寻找数组的中心索引 | Find Pivot Index

    Given an array of integers nums, write a method that returns the "pivot" index of this arr ...

  5. 10 个深恶痛绝的 Java 异常。。

    异常是 Java 程序中经常遇到的问题,我想每一个 Java 程序员都讨厌异常,一 个异常就是一个 BUG,就要花很多时间来定位异常问题. 什么是异常及异常的分类请看这篇文章:一张图搞清楚 Java ...

  6. scala时间和时间戳互转

    时间转换为时间戳: import java.text.SimpleDateFormat object test { def main(args: Array[String]): Unit = { va ...

  7. 10.Django ModelForm

    ModelForm 1.ModeForm简单验证 from django.db import models # Create your models here. class UserInfo(mode ...

  8. 一条命令解决mac版本python IDLE无法输入中文问题

    安装完Python通常自动就有了一个简易的集成环境IDLE,但在mac上,无法在IDLE中使用中文. 通常故障有两种情况: 在IDLE中,中文输入法根本无法工作,不会弹出输入框,所有的输入都被当做英文 ...

  9. scrapy爬虫学习系列五:图片的抓取和下载

    系列文章列表: scrapy爬虫学习系列一:scrapy爬虫环境的准备:      http://www.cnblogs.com/zhaojiedi1992/p/zhaojiedi_python_00 ...

  10. Android Nine-patch(.9.png)小结

    最近在项目开发过程中,因为界面布局美化的需要开始接触到.9.png,无论是Goolge官方文档还是网上其他资料, 都给出了很多关于.9.png的基本介绍,.9.png基础文章推荐以下几篇: Googl ...