目录
1、协程
2、依赖
3、协程启动的三种方式
3.1、runBlocking:T
3.2、launch:Job
3.3、aync/await
4、GlobalScope
5、delay()与sleep()
6、协程的优点:
7、协程的缺点:
8、适用场景
9、子程序
10、进程
11、线程
协程,又称微线程。英文名Coroutine。
官方文档定义:
协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将开发者代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。
协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。
总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 -- 协程挂起。
github地址
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
运行一个新的协程并阻塞当前线程,直到当前协程完成为止。
它相当于线程和协程之间的一个调度,将线程调度到协程中,直到协程完成,在调回到线程中。
这种方式不推荐开启一个协程,它是供在main函数和测试中使用。
从下面的输出结果可以明显看出,runBlocking:T 阻塞了当前Thread的执行。
fun main() {
Thread(Runnable {
repeat(5) {
if (it == 2) {
runBlocking {
repeat(5){
println("runBlocking的it=$it")
}
}
}
println("Thread线程的it=$it")
}
}).start()
}
//输出结果
Thread线程的it=0
Thread线程的it=1
runBlocking的it=0
runBlocking的it=1
runBlocking的it=2
runBlocking的it=3
runBlocking的it=4
Thread线程的it=2
Thread线程的it=3
Thread线程的it=4
在不阻塞当前线程的情况下启动新的协程,并以[Job]返回对协程的引用。
当调用Job.cancel时,协程被取消。
从下面代码可以看出,协程job1可以通过cancel()函数来取消,这就是协程的一大优点,可以主动的控制协程开启和结束。被cancel()的协程无法再次重启。
fun main() {
val job1 = GlobalScope.launch {
repeat(5) {
println("launch的it=$it")
delay(500)
}
}
println("job.isCompleted=${job1.isCancelled}")
Thread.sleep(1500)
job1.cancel()
println("job.isCompleted=${job1.isCancelled}")
job1.start()
}
//输出结果
job.isCompleted=false
launch的it=0
launch的it=1
launch的it=2
job.isCompleted=true
创建一个协程并作为[Deferred]的实现返回其将来的结果。
Deferred.await()可以获取到你返回的值。
fun main() = runBlocking {
val name1:Deferred = async {
val name = "ZhangSan"
name
}
println("name=${name1.await()}")
}
//输出结果
name=ZhangSan
全局范围内启动在整个应用程序生命周期内运行且不会过早取消的顶级协程。
协程启动通常应使用应用程序定义的[CoroutineScope]。 不建议在[GlobalScope]实例上使用[async] [CoroutineScope.async]或[launch] [CoroutineScope.launch]。
GlobalScope.launch {
repeat(10) {
delay(500)
println("launch的it=$it")
}
}
delay() 不阻塞线程
将协程延迟给定时间而不阻塞线程,并在指定时间后恢复它。
此暂停功能可以取消。Thread.sleep() 阻塞线程
使当前正在执行的线程进入休眠状态(暂时停止执行)达指定的毫秒数。
- 协程更加轻量,创建成本更小,降低了内存消耗
每个协程的体积比线程要小得多,因此一个进程可以容纳数量相当可观的协程。
- 协程是由开发者调度,减少了使用线程造成 CPU 上下文切换的开销,提高了 CPU 缓存命中率
协作式调度相比抢占式调度的优势在于上下文切换开销更少、更容易把缓存跑热。和多线程比,线程数量越多,协程的性能优势就越明显。进程 / 线程的切换需要在内核完成,而协程不需要,协程通过用户态栈实现,更加轻量,速度更快。在重 I/O 的程序里有很大的优势。比如爬虫里,开几百个线程会明显拖慢速度,但是开协程不会。
但协程也放弃了原生线程的优先级概念,如果存在一个较长时间的计算任务,由于内核调度器总是优先 IO 任务,使之尽快得到响应,就将影响到 IO 任务的响应延时。假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。
此外,单线程的协程方案并不能从根本上避免阻塞,比如文件操作、内存缺页,这都属于影响到延时的因素。
- 减少同步加锁,整体上提高了性能
协程方案基于事件循环方案,减少了同步加锁的频率。但若存在竞争,并不能保证临界区,因此该上锁的地方仍需要加上协程锁。
- 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调
需要注意的是,协程的确可以减少 callback 的使用但是不能完全替换 callback。基于事件驱动的编程里面反而不能发挥协程的作用而用 callback 更适合。
- 在协程执行中不能有线程的阻塞操作,否则整个线程被阻塞(协程是语言级别的,线程,进程属于操作系统级别)
fun main(){
runBlocking {
launch {
repeat(5) {
delay(200)
println("我是Launche---$it----${Date().time}")
}
}
async {
repeat(5) {
delay(200)
if (it == 2) {
Thread.sleep(5000)
}
println("我是async---$it----${Date()}")
}
}
}
}
//输出结果
我是Launche---0----Wed Dec 25 18:19:21 CST 2019
我是async---0----Wed Dec 25 18:19:21 CST 2019
我是Launche---1----Wed Dec 25 18:19:22 CST 2019
我是async---1----Wed Dec 25 18:19:22 CST 2019
我是Launche---2----Wed Dec 25 18:19:22 CST 2019
----------------------------------------------中间阻塞了5秒
我是async---2----Wed Dec 25 18:19:27 CST 2019
我是Launche---3----Wed Dec 25 18:19:27 CST 2019
我是async---3----Wed Dec 25 18:19:27 CST 2019
我是Launche---4----Wed Dec 25 18:19:27 CST 2019
我是async---4----Wed Dec 25 18:19:27 CST 2019
- 需要特别关注全局变量、对象引用的使用
- 协程可以处理 IO 密集型程序的效率问题,但是处理 CPU 密集型不是它的长处。
假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。
- 高性能计算,牺牲公平性换取吞吐。协程最早来自高性能计算领域的成功案例,协作式调度相比抢占式调度而言,可以在牺牲公平性时换取吞吐
- IO Bound 的任务
在 IO 密集型的程序中由于 IO 操作远远小于 CPU 的操作,所以往往需要 CPU 去等 IO 操作。同步 IO 下系统需要切换线程,让操作系统可以再 IO 过程中执行其他的东西。这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带来了大量的性能的浪费。
所以人们发明了异步 IO。就是当数据到达的时候触发我的回调。来减少线程切换带来性能损失。但是这样的坏处也是很大的,最大的问题就是破坏掉了人类这种线性的思维模式,你必须把一个逻辑上线性的过程切分成若干个片段,每个片段的起点和终点就是异步事件的完成和开始。固然经过一些训练你可以适应这种思维模式,但你还是要付出额外的心智负担。与人类的思维模式相对应,大多数流行的编程语言都是命令式的,程序本身呈现出一个大致的线性结构。异步回调在破坏点思维连贯性的同时也破坏掉了程序的连贯性,让你在阅读程序的时候花费更多的精力。这些因素对于一个软件项目来说都是额外的维护成本,所以大多数公司并不是很青睐 node.js 或者 RxJava 之类的异步回调框架,尽管这些框架能提升程序的并发能力。
但是协程可以很好解决这个问题。比如把一个 IO 操作 写成一个协程。当触发 IO 操作的时候就自动让出 CPU 给其他协程。要知道协程的切换很轻的。协程通过这种对异步 IO 的封装既保留了性能也保证了代码的容易编写和可读性。
- Generator 式的流式计算
消除 Callback Hell(回调地狱),使用同步模型降低开发成本的同时保留更灵活控制流的好处,
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意:在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。
程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的概念主要有两点:
第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。